테스트 코드 학습(Mockito)
이전 글에서 Junit5에 대해서 정리하였습니다. 이번 글은 Junit5에서 Mockito를 사용하는 방법에 대해서 정리하겠습니다.
사용된 모든 소스는 Github에 올려두었습니다.
참고 영상 : www.inflearn.com/course/the-java-application-test/dashboard
Mockito란?
이전에 작성한 이 글과 함께 보시면 이해하시는 데 도움이 될 것입니다. Spring Camp - 테스트 코드에 대하여
테스트 코드를 작성할 때 아름다운 그림은 모든 인스턴스가 완성되어 있고 외부 통신 등 non-testable의 경우가 없는 것일 것 같습니다. 하지만 실전은 그렇게 마음대로 되지 않을 때가 많습니다. 인스턴스 메서드가 완성되지 않고 non-testable 영역도 있고.... 이런 경우 메서드가 완료될때까지 기다려야합니다. non-testable 영역은.. 포기..
이런 경우 Mockito 즉 가짜를 만들어서 내가 요청을 주면 원하는 응답이 나오는 가짜 인스턴스를 만들어 테스트를 할 수 있는 환경을 구축합니다.
public class ZooService {
private final AnimalService animalService;
private final ZooRepository zooRepository;
public ZooService(AnimalService animalService, ZooRepository zooRepository) {
assert animalService != null;
assert zooRepository != null;
this.animalService = animalService;
this.zooRepository = zooRepository;
}
public Animal enterAnimal(Long animalId) {
Optional<Animal> optionalAnimal = animalService.findById(animalId);
Animal animal = optionalAnimal.orElseThrow(() -> new IllegalArgumentException("animal 이 존재하지 않습니다."));
return zooRepository.save(animal);
}
}
위와 같은 코드가 있습니다. 하지만 AnimalService는 interface만 있고 ZooRepository는 DB와 연동되지 않은 상태입니다. 이러한 코드를 테스트 할 때 Mockito를 사용하면 유용합니다.
Mockito 선언 방법
인스턴스 생성자 생성
테스트 메서드 내 직접 생성자로 구현하는 방법이 있습니다.
class ZooServiceTest {
@Test
@DisplayName("ZooService 인스턴스 생성자로 생성")
void createZooService1() {
AnimalService animalService = animalId -> Optional.empty();
ZooRepository zooRepository = animal -> null;
ZooService zooService = new ZooService(animalService, zooRepository);
assertNotNull(zooService);
}
}
Mockito.mock 사용
mock 메서드를 사용하여 원하는 클래스를 생성하는 방법입니다.
import static org.mockito.Mockito.mock;
class ZooServiceTest {
@Test
@DisplayName("ZooService 인스턴스를 Mock을 사용하여 생성")
void createZooService2() {
AnimalService animalService = mock(AnimalService.class);
ZooRepository zooRepository = mock(ZooRepository.class);
ZooService zooService = new ZooService(animalService, zooRepository);
assertNotNull(zooService);
}
}
@Mock
MockitoExtension으로 확장하고 Mock Annotation을 사용하여 Mock 객체를 만들 수 있습니다.
@ExtendWith(MockitoExtension.class)
class ZooServiceTest {
@Mock
AnimalService animalService;
@Mock
ZooRepository zooRepository;
@Test
@DisplayName("ZooService 인스턴스를 Annotation Mock을 사용하여 생성")
void createZooService3() {
ZooService zooService = new ZooService(animalService, zooRepository);
assertNotNull(zooService);
}
}
변수 @Mock
변수에 Mock 어노테이션을 사용하여 Mock 객체를 주입합니다.
@ExtendWith(MockitoExtension.class)
class ZooServiceTest {
@Test
@DisplayName("ZooService 인스턴스 변수에 Annotation Mock을 사용하여 생성")
void createZooService4(@Mock AnimalService animalService, @Mock ZooRepository zooRepository) {
ZooService zooService = new ZooService(animalService, zooRepository);
assertNotNull(zooService);
}
}
Stubbing
Stubbing이란 위에서 생성한 Mock객체에 대한 응답을 지정하는 것을 말합니다.
응답 지정방법으로는 Mockito에서 제공하는 when(), then 메서드를 이용해서 지정할 수 있습니다.
when, then
@ExtendWith(MockitoExtension.class)
class ZooServiceTest {
@Mock
AnimalService animalService;
@Mock
ZooRepository zooRepository;
@Test
@DisplayName("동물원 서비스에서 조회")
void animalService_findById_test() {
ZooService zooService = new ZooService(animalService, zooRepository);
assertNotNull(zooService);
Animal animal = new Animal();
animal.setName("호랑이");
when(animalService.findById(any()))
.thenReturn(Optional.of(animal))
.thenThrow(new RuntimeException())
.thenReturn(Optional.empty());
Optional<Animal> byId = animalService.findById(1L);
assertThat(byId.get().getName()).isEqualTo(animal.getName());
assertThrows(RuntimeException.class, () -> {
animalService.findById(2L);
});
Optional<Animal> byId1 = animalService.findById(3L);
assertThat(byId1).isEqualTo(Optional.empty());
}
}
when, then 메서드를 통해 animalService.findById의 return 값을 3개 설정하였습니다.
- Optional.of(animal)
- new RuntimeExecption
- Optional.empty
위 3개에 대한 assert 검증을 하면 모두 정상이 나타납니다.
그럼 다시 돌아가서 실제 ZooService의 enterAnimal 메서드가 정상적으로 작동 될 수 있도록 적용해보겠습니다.
class ZooServiceTest {
@Test
@DisplayName("동물원 서비스에서 조회")
void animalService_findById_test() {
ZooService zooService = new ZooService(animalService, zooRepository);
assertNotNull(zooService);
Animal animal = new Animal();
animal.setId(1);
animal.setName("호랑이");
when(animalService.findById(1))
.thenReturn(Optional.of(animal));
when(zooRepository.save(animal))
.thenReturn(animal);
Animal resultAnimal = zooService.enterAnimal(1);
assertEquals(animal, resultAnimal);
}
}
verify
public class ZooService {
public Animal enterAnimal(Integer animalId) {
Optional<Animal> optionalAnimal = animalService.findById(animalId);
Animal animal = optionalAnimal.orElseThrow(() -> new IllegalArgumentException("animal 이 존재하지 않습니다."));
animalService.notify(animal); // 추가
return zooRepository.save(animal);
}
}
enterAnimal 메서드에서 결과값이 없는 메서드를 호출하는 로직을 추가하였습니다. 테스트 코드에서 위 메서드를 호출하였는 지 체크하는 방법에 대해서 알아보겠습니다.
@ExtendWith(MockitoExtension.class)
class ZooServiceTest {
@Test
@DisplayName("동물원 서비스에서 조회")
void animalService_findById_test() {
// ...
verify(animalService, times(1)).notify(animal);
verify(animalService, never()).validate(animal);
// ...
}
}
Mockito에서 제공하는 verify 메서드를 사용하면 호출 횟수에 대해서도 테스트가 가능합니다.
inOrder
특정 클래스에 대한 호출 순서또한 확인하고 싶을 때 사용합니다. 위 로직에서 animalService의 메서드는 findById, notify 메서드 순으로 호출하였습니다. 테스트 방법에 대해서 알아보겠습니다.
@ExtendWith(MockitoExtension.class)
class ZooServiceTest {
@Test
@DisplayName("동물원 서비스에서 조회")
void animalService_findById_test() {
// ...
InOrder inOrder = inOrder(animalService);
inOrder.verify(animalService).findById(any());
inOrder.verify(animalService).notify(any());
// ...
}
}