TDD와 함께 정리하려고 하다가,
따로 정리하는게 나중에 보기 쉬울 것 같아서 포스팅을 따로 하기로 결정했다.
예전에 테스트 코드 작성할 때, 팀원 한 명은 Mock을 이용해 테스트 코드를 작성했고, 나는 Mock을 사용하지 않았었다.
그 당시에는 Mock을 쓸 줄 몰랐기 때문에, 지금에서 Mocking에 대해 개념을 정리해보고 왜 사용해야하는지 알아보자.
Mock: 모방하다
말 그대로, 가짜 객체를 만들어주는 framework가 바로 Mockito다.
그럼 테스트코드에서 왜 Mock을 사용해야할까?
우리는 개발자이기 때문에 말로 설명하는 것 보다 코드로 보는게 훨씬 이해가 빠르기 때문에 코드로 직접 봐보자.
(예시코드)
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository){
this.userRepository = userRepository;
}
public String getUserName(Long userId) {
User user = userRepository.findById(userId);
return user.getUserName();
}
}
UserService 클래스를 살펴보자.
간단하게, repository에서 user를 검색하고,
user의 이름을 반환하는 코드다.
이 코드의 테스트 코드를 작성한다고 생각해보자.
@BeforeEach
void setUp(){
UserRepository userRepository = new UserRepository();
User user = new User(1L, "userA");
userRepository.save(user);
UserService userService = new UserService(userRepository); // 의존성을 직접 전달
String findUserName = userService.getUserName(1L);
assertThat(findUserName).isEqualTo(user.getName());
}
뭐 대충 이런 식이지 않을까?
중요한 점은, userService를 테스트 하기 위해,
실제 userRepository에 값을 저장하는 과정이 필요하다는 것이다.
간단한 테스트이기에, 코드가 짧지만, 여러 의존관계가 섞여있다면,
userService의 getUserName() 메서드를 테스트 하기 위해서 상당히 많은 코드를 작성해야 할 것이다...!
이제 Mock을 사용한 코드를 살펴보자
public class UserServiceTest {
@Mock
private UserRepository userRepository; // 가짜 객체로 의존성 주입
@InjectMocks
private UserService userService; // UserRepository가 주입된 UserService 객체 생성
@Test
void getUserName_method_test() {
// given
User user = new User(1L, "testA");
when(userRepository.findById(1L)).thenReturn(user); // Mock 설정: findById 호출 시 가짜 User 반환
// when
String findUserName = userService.getUserName(1L); // Mock 데이터 기반으로 동작 검증
// then
assertThat(findUserName).isEqualTo(user.getName()); // 결과 검증
}
}
실제 데이터베이스에 값을 저장하는 로직을 만들지 않고,
오직 "userService.getUserName()" 메서드의 테스트에만 집중할 수 있다.
테스트를 하기 위해 여러 의존성을 직접 만드는 것 보다는
엄청 간편한 것 같다.
주의점 : 무분별한 Mocking을 사용해서는 안 된다.
통합테스트와 같은 실제 테스트를 위해서는
Mocking보다는 실제로 로직 테스트를 위해 의존성을 직접 삽입하는걸 고려해보자.
Mocking은 단위테스트에만 사용하도록 노력해보자!
또한, 무조건 Mocking을 한다고 생각하지 말자.
Mocking을 사용함으로써 간단하게 적으려고 하는 코드가
오히려 더 복잡해질 수 있다.
상황에 따라 알맞게 판단해보자.
필자의 경우, Service로직에는 웬만하면 의존성을 직접 주입할 것 같고, Controller의 경우에만 Mocking을 사용하도록 할 것이다.
(물론 Service 로직에도 Mocking을 사용할 경우가 생기고, 코드가 훨씬 간편해진다면 Mockig을 사용할 예정)
이제 사용 이유에 대해 알아보았으니, 사용 방법과 문법에 대해 알아보자.
JUnit 4를 사용하는 경우,
@RunWith(MockitoJUnitRunner.class)
어노테이션을 사용해야한다.
JUnit 5를 사용하는 경우에 @ExtendWith(MockitoExtension.class) 사용
위의 어노테이션이 하는 역할은
Mockito와 JUnit을 연결하는 역할이다.
@ExtendWith(MockitoExtension.class) 사용시에
@Mock 어노테이션 및 @InjectMocks 어노테이션 사용 가능함
@ExtendWith 사용 안 할 경우 수동으로 Mock 객체를 생성해줘야 함
(예시코드)
@BeforeEach
void setUp() {
// 수동으로 Mock 객체 생성
userRepository = Mockito.mock(UserRepository.class);
userService = new UserService(userRepository); // 의존성 주입
}
@Test
void getUserName_method_test() {
User user = new User(1L, "testUser");
Mockito.when(userRepository.findById(1L)).thenReturn(user); // Mock 동작 설정
String result = userService.getUserName(1L);
assertEquals("testUser", result);
}
웬만하면 JUnit5를 사용하는 경우 @ExtendWith를 사용하겠지만,
어노테이션의 역할에 대해서는 확실하게 알아가야 할 것 같아 정리했다.
when/given은 똑같은 역할이지만 네이밍만 다르다.
BDD (given/when/then) 패턴에 맞추어 테스트 코드를 작성할 예정이라면, given을 사용해서 네이밍을 깔끔하게 하자!
when(mock.method()).thenReturn(value);
mock.method가 호출되었을 때 지정된 value값을 리턴해준다.
when(mock.method()).thenThrow
(new RuntimeException("Error"));
특정 호출에서 예외를 던지도록 설정한다.
verify():
verify(mock).someMethod();
verify(mock, times(2)).someMethod();
verify(mock, never()).someMethod();
verify(mock).someMethod("expectedArg");
Mock 객체의 메서드가 특정 조건에 맞게 호출되었는지 확인 가능.
Mock 객체의 메서드가 몇 번 호출되었는지, 특정 인자로 호출되었는지 등을 검증
ArgumentCaptor<String> captor ArgumentCaptor.forClass(String.class);
verify(mock).someMethod(captor.capture());
assertEquals("expectedValue", captor.getValue());
Mock 객체의 메서드가 호출될 때 전달된 인자들을 캡처하고, 이를 나중에 확인
Spy 객체는 실제 객체의 동작을 감시하면서 특정 메서드만 모킹 가능
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
// 실제 메서드 호출
spyList.add("one");
// 특정 메서드는 모킹
when(spyList.size()).thenReturn(100);
assertEquals(100, spyList.size()); // 모킹된 동작
assertEquals("one", spyList.get(0)); // 실제 메서드 호출
doReturn()/ doThrow()/ doANswer()
when() 으로 설정하기 어려운 상황에서 사용.
특히 void 메서드를 모킹할 때 유용함
doReturn(value).when(mock).method();
when() 과 달리, 메서드 호출 전에 실제 동작을 제어할 수 있음
편리한 도구가 하나 늘었다고 생각하면 좋을 것 같다.
너무 Mocking에 의존하지 말고, 의존성이 복잡하게 연결되어있어 테스트 코드 작성이 너무 오래걸리고, 복잡할 때
사용하면 좋은 framework 인 것 같다.