Unit test - Test Double과 Mockito

Spring boot 프로젝트에서 unit test를 진행해보자

 

프로젝트를 하다가 문득 테스트도 같이 진행해보면 좋을 것 같다는 생각을 했다. 아직 제대로 해본 적이 없어서 공부할 겸 통합테스트, 유닛테스트에 관해 찾아보았다. 통합테스트는 JPA 공부하면서 많이 사용해봤는데 유닛테스트는 처음이어서 모르는 용어 투성이였다.

 

그래서 유닛테스트 공부의 시작으로, 가짜 객체의 개념인 Test Double과 Mock을 만들어 테스트할 수 있는 Mockito에 대해 알아보려 한다!

Test Double

Integral test에서는 Controller, Service, Repository가 한 번에 연결되어서 테스트되는 반면, Unit test에서는 각 unit을 검사하는 것이기 때문에 연관된 객체를 사용하기 힘들다. 그래서 원래의 객체와 비슷하면서도 단순화된 객체가 대신 필요한데, 이를 Test double이라고 한다.

 

Test double은 수행하는 역할에 따라 5가지로 구분할 수 있다.

  1. Dummy object
    • 전달은 되지만 사용되지 않는 객체. 전달하기 위해 사용
  2. Fake object
    • 동작은 하지만 실제 사용되는 객체처럼 정교하게 동작하지는 않는 객체
  3. Test stub
    • 실제로 동작하는 것처럼 테스트에서 호출에 대해 미리 준비된 결과를 받아서 제공하는 객체
    • 테스트를 위해 의도한 결과만 반환되도록 하기 위한 객체
  4. Test spy
    • stub의 역할을 가지면서 호출 방법에 따라 일부 정보를 기록함
    • 실제 객체로도 사용 가능하고, Stub 객체로도 활용할 수 있으며 필요한 경우 특정 메서드가 제대로 호출되었는지 여부도 확인이 가능함
    • ex. 전송된 메시지 수를 기록하는 이메일 서비스
  5. Mock object
    • 예상되는 호출을 명세하고 내용에 따라 동작하도록 프로그래밍 된 객체
    • 예상되는 호출을 뿐만 아니라 예상치 못한 호출이라면 예외를 던지고, 모든 호출을 다 받았는지 확인할 수도 있음

밑으로 갈수록 수행 역할의 범위가 넓어지고 Mock 객체가 가장 강력하다고 할 수 있다.

 

Mockito

Mockito는 Java에서 unit test를 할 수 있도록 가짜 객체를 지원하는 Mocking Framework이다.

그럼 이제부터 가짜 객체를 만들어 볼 것인데, Mockito에서는 어노테이션으로 어렵지 않게 만들 수 있다!

주로 사용되는 어노테이션을 살펴보자.

 

@Mock

  • Mock 객체를 만드는 어노테이션으로, 객체를 생성하여 주입시키면 행동이나 동작을 지정해줄 수 있음
  • when()이나 given()을 통해 mock의 행동을 지정할 수 있음
  • Answer 인터페이스를 통해 동작을 지정할 수 있으며, 직접 custom도 가능함

@Spy

  • Spy 객체를 만드는 어노테이션으로, Stubbing된 기능이 아닌 실제 기능을 사용하고 싶을 때 사용
  • 실제 메서드가 호출되지만 verify(검증)와 stub이 가능

@InjectMocks

  • @Spy 또는 @Mock이 달린 필드들을 감지하여 자동으로 주입시켜줌

 

예를 들어 PostService를 테스트한다고 가정해보자.

PostService는 메서드마다 PostRepository를 사용할테고 PostService만 테스트해주기 위해서는 PostRepository를 가짜 객체로 만들어주면 된다.

@ExtendWith(MockitoExtension.class)
class PostServiceTest {
    @InjectMocks
    private PostService postService;

    @Mock
    private PostRepository postRepository;

	...
}

PostRepository가 가짜 객체이므로 @Mock 어노테이션을 달아주면 되고, Mock 객체를 주입받는 PostService에는 @InjectMocks 어노테이션을 적어주면 된다.

여기서 가짜로 생성된 메서드가 아닌 실제 메서드를 사용하고 싶다면, 그 객체 위에 @Spy를 적어 Spy 객체로 만들어주면 된다.

Stubbing

Mock 객체를 만들었으니 예상되는 호출을 지정해주고 원하는 대로 동작하게 만들어줘야 하는데, 만들어진 Mock 객체의 메서드를 실행했을 때 어떤 결과를 제공할지 정의하는 것을 Stubbing이라고 한다.

 

Mockito에서는 이 stubbing을 만드는 방법이 두 가지 있다.

1. when().thenDoSomething()

when()으로 가짜 객체의 어떤 메서드를 호출시킬지 정의한 후에, 그 행동인 결과를 then~()으로 지정하는 방식이다.

 

interface Employee {
    String greet();
    void work(DayOfWeek day);
}

greet() 메서드를 실행시키면 “Hello”를 return 시키도록 설정한 Mock 객체 Employee가 있다고 가정해보자.

@Test
void givenNonVoidMethod_callingWhen_shouldConfigureBehavior() {
    // given
    when(employee.greet()).thenReturn("Hello");

    // when
    String greeting = employee.greet();

    // then
    assertThat(greeting, is("Hello"));
}
  • when()을 호출해 가짜 객체의 메서드를 호출시키고, thenReturn()을 통해 “Hello”를 리턴시키도록 설정하는 것
  • 다만, 이 방법은 void를 return 시킬 수 없고, 다른 메서드의 호출로 묶을 수도 없다.

 

thenDoSomething 자리에는 인터페이스 OngoingStubbing의 메서드가 들어간다.

  • then(Answer<?> answer), thenAnswer(Answer<?> answer) : 메서드 실행 후 어떤 행동을 할 지 설정
  • thenCallRealMethod() : 가짜 객체가 가진 메서드 실행 후 진짜 메서드를 호출하는 것
  • thenReturn(T value[, T… values]) : 메서드 실행 후 어떤 객체를 return할 지 설정
  • thenThrow(Class<? extends Throwable> throwableType) : 메서드 실행 후 어떤 예외를 던질 지 설정

 

2. doSomething().when()

이 방식은 행동의 결과를 먼저 do~()로 지정한 후, 호출될 메서드를 when()을 통해 지정하면 된다.

 

 

똑같이 Employee 객체가 주어져 있을 때 테스트 방식은 다음과 같다.

@Test
void givenVoidMethod_callingDoThrow_shouldConfigureBehavior() {
    // given
    doThrow(new IAmOnHolidayException()).when(employee).work(DayOfWeek.SUNDAY);

    // when
    Executable workCall = () -> employee.work(DayOfWeek.SUNDAY);

    // then
    assertThrows(IAmOnHolidayException.class, workCall);
}
  • 먼저 제공되는 결과인 예외 발생을 doThrow()로 정의시킨 다음에, when()을 통해 가짜 객체의 메서드를 호출시킴
  • 이 방식은 void 메서드에 대해 테스트를 할 수 있고, 같은 동작을 chain 형식으로 반복해서 사용할 수 있다는 장점이 있음

 

doSomething 자리에는 인터페이스 Stubber의 메서드가 들어간다.

  • doAnswer(Answer answer) : 메서드 실행 후 어떤 작업을 할 지 설정
  • doNothing() : 메서드 실행 후 아무 행동도 하지 않도록 설정
  • doReturn(Object toBeReturned) : 메서드 실행 후 어떤 행동을 할 지 설정
  • doCallRealMethod() : 실제 메서드 호출
  • doThrow(Throwable… toBeThrown) : 메서드 실행 후 어떤 예외를 던질 지 설정

 

Unit test 적용해보기

가짜 객체를 만드는 방법을 배웠으니 테스트코드를 작성해보았다.

@Test
public void whenSavePost_shouldReturnPost() {
    // given
    Post post = new Post(1L, "title1", "content1");
    when(postJpaRepository.save(post)).thenReturn(post);

    // when
    Post result = postService.createPost(post);

    // then
    assertEquals(post, result);
}
@Test
public void whenFindPost_shouldReturnPost() {
    // given
    Post post = new Post(1L, "title1", "content1");
    when(postRepository.findById(1L)).thenReturn(post);

    // when
    Post result = postService.findPost(1L);

    // then
    assertEquals(post, result);

}
@Test
public void whenFindPost_shouldReturnPost() {
    // given
    Post post = new Post(1L, "title1", "content1");
	doReturn(post).when(postRepository).findById(1L);

    // when
    Post result = postService.findPostTest(1L);

    // then
	assertEquals(post, result);

}

 

다음에는 검증하는 방법인 verify와 BDD 스타일에 대해서도 정리해볼 것이다.

 

References