테스트는 최대한 단위 테스트로 진행하라고 배웠던 것 같다.
하나의 테스트에서 2개의 메서드를 테스트를 할 때, 에러가 발생하면 어디에서 발생한 에러인지 찾기 힘들기 때문이다.
그렇기에 최대한 쪼개서 하나하나 테스트를 진행하는 것이 좋다고 배웠고, 그렇게 진행하고 있다.
복잡한 의존관계 속의 테스트
현재 UserService는 3가지 대상을 주입받아 실행되고 있다.
UserDao, MailSender, PlatformTransactionManager 이렇게 3개이다.
내가 UserService에서 테스트하고 싶은 것은 사용자의 비즈니스 로직일 것이다.
하지만 지금과 같은 상황에서는 데이터베이스와 같은 뒷단의 서비스에도 영향을 받게 되었다.
만약 비즈니스 로직이 맞더라도, 데이터베이스에서 에러가 발생한다면 해당 테스트는 통과하지 못하는 것이다.
일단 테스트를 진행하기 위해서는 이런 부분부터 해결해야 한다.
테스트 대상 오브젝트 고립시키기
일단 테스트의 대상을 환경이나 다른 오브젝트들에게 영향을 받지 않도록 고립시켜보자.
의존하는 클래스들을 주입받는 것이 아닌, 테스트 대역을 주입받고 원하는 역할을 수행하도록 하는 것이다.
저번에 작성했던 MailSenderTest와 같은 맥락이다.
일단 UserServiceImpl을 테스트해보자.
현재 UserServiceImpl에서는 2가지에 의존하고 있다.
트랜잭션관련 부분은 UserServiceTx로 추출했기에 현재 UserServiceImpl에서는 신경을 쓰지 않는다.
그렇기에 지금 신경쓰는 클래스들은 UserDao, MailSender이다.
만약 UserDao를 그냥 테스트 대역으로 만든다면, UserDao에 정상적으로 값이 요청되었는지는 확인하기 힘들어진다.
데이터베이스에 값이 저장되지는 않기 때문이다.
물론 데이터베이스에 값이 저장되는지 확인하는 것은 UserServiceImpl의 책임은 아니긴 하니, 적어도 UserDao에게 제대로 값이 요청되었는지는 확인을 해야한다.
그렇기에 UserDao를 mock으로 만들어보자.
static class MockUserDao implements UserDao{
private final List<User> users;
@Getter
private final List<User> updatedUsers = new ArrayList<>();
public MockUserDao(List<User> users) {
this.users = users;
}
@Override
public void add(User user) {
}
@Override
public User get(String id) {
return null;
}
@Override
public List<User> getAll() {
return this.users;
}
@Override
public void deleteAll() {
}
@Override
public int getCount() {
return 0;
}
@Override
public void update(User user) {
updatedUsers.add(user);
}
}
안쓰는 메서드는 대충 두고, 나중에 요청한 값들 볼 수 있도록 메서드를 추가적으로 만들어둔다.
그리고 upgradeLevels()를 다음과 같이 수정하고
@Test
public void upgradeLevels() {
MailSenderTest mailSenderTest = new MailSenderTest();
MockUserDao mockUserDao = new MockUserDao(this.users);
UserServiceImpl userServiceImpl = new UserServiceImpl(mockUserDao, mailSenderTest);
userServiceImpl.upgradeLevels();
List<User> updated = mockUserDao.getUpdatedUsers();
Assertions.assertEquals(Level.SILVER, updated.get(0).getLevel());
Assertions.assertEquals(Level.GOLD, updated.get(1).getLevel());
}
테스트를 돌려보니
이렇게 성공하는 것을 볼 수 있다.
UserDao의 Mock을 만들어서 데이터베이스에 접근하지 않고도 테스트의 결과를 확인 할 수 있도록 만들었다.
심지어 데이터베이스에 접근도 하지 않으니, 테스트의 시간도 더 짧아졌을 것이다.
단위 테스트와 통합 테스트
자 일단 단위테스트는 '테스트 대상 클래스를 목 오브젝트 등의 테스트 대역을 이용해 의존 오브젝트나 외부의 리소스를 사용하지 않도록 고립시켜서 테스트 하는 것'을 말하며, 반드시 필요하다.
반면 '클래스끼리 상호작용이나, 외부 DB가 참여하여 진행하는 테스트'를 통합 테스트를 통합테스트라고 한다.
우리가 저번에 만들었던 Dao와 같은 클래스는 단위 테스트를 진행하지 못한다.
목적 자체가 데이터베이스에 데이터를 저장하는 것이기 때문에, 독립되어있는 단위 테스트는 의미가 없기 때문이다.
상황에 맞게 테스트를 적절하게 작성하여, 충분한 테스트를 진행하도록 하자.
테스트가 없으면 리펙토링이 어렵고, 사실상 확장 할 수 없는 소프트웨어가 된다.
목 프레임워크
단위 테스트를 진행하기 위해 목 오브젝트를 만들었었다.
단위 테스트를 위해 무조건 필요한 존재이지만, 항상 작성하기에는 너무 번거롭다.
그렇기에 이런 기능을 지원해주는 Mockito 프레임워크를 사용해보자.
Mockito를 사용하면 목 클래스를 일일이 만들 필요가 없다.
@ExtendWith({MockitoExtension.class})
public class UserServiceTest {
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserDao userDao;
@Mock
private MailSender mailSender;
}
테스트 자체어 MockitoExtension을 설정해주고, 테스트를 해야하는 대상에 @InjectMocks를, 주입으로 사용하기 위한 오브젝트들은 @MockBean으로 설정해주면 @InjectMocks에 주입이 된다.
이 때 @InjectMocks는 인터페이스가 아닌 클래스로 구현해야 하기에, 여기에서는 UserServiceImpl로 만들어주었다.
이렇게 만들어준 MockBean들은 그냥 빈 껍데기이다.
when 메서드를 이용해 해당 인스턴스의 메서드가 호출되는 경우의 값을 지정해준다.
Mockito.when(userDao.getAll()).thenReturn(this.users);
upgradeLevels() 메서드는
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user : users) {
if (user.isUpgradeLevel()) upgradeLevel(user);
}
}
이렇게 작성되어 있는데, 여기서 userDao를 호출하면 when에서 지정된 값이 반환되는 것이다.
@Test
public void upgradeLevels() throws MessagingException {
//given
//userService에서 userDao.getAll()를 호출하면 반환할 값을 지정
Mockito.when(userDao.getAll()).thenReturn(this.users);
//when
userService.upgradeLevels();
//then
//userDao의 update 메서드가 몇번 호출되었는지 검증
verify(userDao, times(2)).update(any(User.class));
//userDao의 update 메서드가 users.get(1)으로 몇번 호출되었는지 검증
verify(userDao, times(1)).update(users.get(1));
//userDao의 update 메서드가 users.get(3)으로 몇번 호출되었는지 검증
verify(userDao, times(1)).update(users.get(3));
Assertions.assertEquals(Level.SILVER, users.get(1).getLevel());
Assertions.assertEquals(Level.GOLD, users.get(3).getLevel());
//mailSender로 넘어간 값을 추적하기 위해 사용
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
//mailSender의 sendUpgradeEmail로 넘어간 값들을 가져옴
verify(mailSender, times(2)).sendUpgradeEmail(captor.capture());
List<User> argumentUsers = captor.getAllValues();
Assertions.assertEquals(users.get(1), argumentUsers.get(0));
Assertions.assertEquals(users.get(3), argumentUsers.get(1));
}
테스트 코드는 이렇게 작성해보았다.
지금은 ArgumentCaptor와 verify를 모두 사용했는데, 다음부터는 그냥 verify만 사용해서 테스트 할 거 같다.
둘 다 호출되어서 넘어간 값들을 추적하는 것이기 때문이다.
Mockito의 사용법은 이정도만 작성을 할 것이지만, 내가 테스트 코드를 작성할 때마다 가장 중요한 툴이라고 항상 생각하는 친구이다.
필요할 때마다 꾸준히 사용법을 익히고 사용하도록 하자.
'Spring > 토비의 스프링' 카테고리의 다른 글
[토비의 스프링] 6.4 스프링의 프록시 팩토리 빈 (0) | 2025.06.19 |
---|---|
[토비의 스프링] 6.3 다이내믹 프록시와 팩토리 빈 (3) | 2025.06.17 |
[토비의 스프링] 6.1 트랜잭션 코드의 분리 (1) | 2025.06.14 |
[토비의 스프링] 5.4 메일 서비스 추상화 (0) | 2025.06.11 |
[토비의 스프링] 5.3 서비스 추상화와 단일 책임 원칙 (0) | 2025.06.09 |