Study/test

테스트 코드 학습(Mockito)

에디개발자 2021. 3. 18. 07:00
반응형

이전 글에서 Junit5에 대해서 정리하였습니다. 이번 글은 Junit5에서 Mockito를 사용하는 방법에 대해서 정리하겠습니다. 

 

사용된 모든 소스는 Github에 올려두었습니다.

 

참고 영상 : www.inflearn.com/course/the-java-application-test/dashboard

 

더 자바, 애플리케이션을 테스트하는 다양한 방법 - 인프런 | 강의

자바 프로그래밍 언어를 사용하고 있거나 공부하고 있는 학생 또는 개발자라면 반드시 알아야 하는 애플리케이션을 테스트하는 다양한 방법을 학습합니다., 그냥 개발자를 넘어 '더 나은 개발

www.inflearn.com

나를 닮았다고 한다...

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());
		
        // ...
    }
}
반응형