Mockito 사용하기

4 minute read

백기선 : 더 자바, 애플리케이션을 테스트하는 다양한 방법 강의를 공부한 내용입니다.

github 소스코드


Mockito

Mockito는 자바의 단위 테스트를 도와주는 오픈소스 프레임워크이다.
웹 Application은 많은 의존성을 주입받아 개발하게 된다.
예를들어 Service라면, DAO 혹은 Repository의 의존성은 거의 기본으로 깔고 들어간다.

만약 테스트 코드를 작성하려고 하는데 로직 테스트가 아닌, 단위 테스트를 하고 싶다면 어떻게 할까?

@Service
@RequiredArgsConstructor
public class EventService {
    
    private final UsersService usersService;
    private final EventRepository eventRepository;
    
    ...

}

public interface EventRepository extends JpaRepository<Event, Long> {
}

public interface UsersService {

    Users findById(Long userId);

    void valid(Long usersId);

}

위의 EventService는 EventRepository와 UserRepository의 의존성을 주입 받는다.

참고로 @RequiredArgsConstructor는 private final 키워드가 붙은 필드를
생성자를 통해 주입 받도록 도와주는 Lombok의 Annotation이다.

만약 EventService를 new 키워드를 사용해서 직접 만들어 사용하려면
먼저 EventRepository와 UsersService를 만들어야한다.
UsersService는 그렇다 쳐도
EventRepository는 JPA를 사용했기 때문에
JpaRepository를 구현해줘야한다.

직접 구현하려면 무려 28개의 메서드를 구현해야한다…
굉장히 귀찮을 것이다.

EventRepository eventRepository = new EventRepository() {
    @Override
    public List<Event> findAll() {
        return null;
    }

    @Override
    public List<Event> findAll(Sort sort) {
        return null;
    }

    @Override
    public List<Event> findAllById(Iterable<Long> iterable) {
        return null;
    }

    @Override
    public <S extends Event> List<S> saveAll(Iterable<S> iterable) {
        return null;
    }
    
    ...

};

UsersService usersService = new UsersService() {
    @Override
    public Users findById(Long usersId) {
        return null;
    }
};

EventService eventService = new EventService(usersService, eventRepository);

이런 불편함을 덜어주기 위해 EventService의 Mock을 만들어 주입을 시켜줄 수 있는데
이때 사용되는 프레임워크가 바로 Mockito이다.


Mock

Mock은 무엇일까?
Mock은 단순히 가짜 객체를 의미한다.
EventRepository와 UsersService를 가짜로 생성해서
EventService를 만들 수 있다.


의존성

먼저 스프링 부트를 사용하고 있다면 Mockito는 기본으로 사용할 수 있다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

spring-boot-starter-test.pom을 확인해 보면
Mockito 의존성을 주입해주는 것을 확인할 수 있다.

<!-- spring-boot-starter-test-2.5.2.pom -->

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.9.0</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>3.9.0</version>
    <scope>compile</scope>
</dependency>

레거시를 사용한다면 위 두개의 의존성을 주입해주면 된다.
mockito-core는 Mockito의 코어 로직이 있고,
mockito-junit-jupiter는 Junit과의 연결을 해주는 역할을 한다.


Mockito 사용하기

Mockito를 사용하는 방법은 크게 세 가지가 있다.

class EventServiceTest {

    @Test
    void createEventService() {
        EventService eventService = Mockito.mock(EventService.class);

        assertNotNull(eventService);
    }

}

첫번째로 static 메서드인 Mockito.mock(Class<?>)를 사용할 수 있다.
당연히 테스트는 통과한다.

@ExtendWith(MockitoExtension.class)
class EventServiceTest {

    @Mock
    UsersService usersService;

    @Mock
    EventRepository eventRepository;


    @Test
    void createEventService() {
        EventService eventService = new EventService(usersService, eventRepository);

        assertNotNull(eventService);
    }

}

다음은 @Mock 애노테이션을 통해 각각의 Mock 객체를 직접 만드는 방법이다.
이때는 필수적으로 @ExtendWith(MockitoExtension.class)를 사용해야 한다.

@ExtendWith(MockitoExtension.class)
class EventServiceTest {

    @Test
    void createEventService(@Mock UsersService usersService,
                            @Mock EventRepository eventRepository) {
        EventService eventService = new EventService(usersService, eventRepository);

        assertNotNull(eventService);
    }

}

마지막으론 메서드의 파라미터로 받을 수 있다.
역시 마찬가지로 @ExtendWith(MockitoExtension.class)를 필수적으로 사용해야 한다.


@ExtendWith

JUnit4에서는 보통 @RunWith를 통해 테스트의 실행 방법을 확장했는데,
JUnit5가 출시되면서 @RunWith가 사라지고, @ExtendWith가 만들어졌다.
앞서 의존성추가에서 말했던 mockito-junit-jupiter 이 의존성안에
@ExtendWith가 포함되어 있다.


Mock 객체

@Mock을 통해 주입받은 Mock 객체의 메서드는
기본적으로 Null, Primitive 타입은 기본 값을,
컬렉션은 비어있는 컬렉션을 반환하며
void 메서드를 호출한다면 예외를 던지지 않고 아무 일도 발생하지 않는다.

@Test
void mockReturnType(@Mock UsersService usersService,
                    @Mock EventRepository eventRepository) {

    Users user = usersService.findById(1L);
    assertNull(user);

}

 @Test
void mockitoVoidMethod() {
    // 아무 일도 발생하지 않는다.
    usersService.duplicateCheck(10000L);
}

테스트는 성공한다.


Stubbing

@Mock을 사용하면 객체가 비어있거나, Null을 반환한다고 했었다.
Stubbing은 이 객체가 실제로 동작하는 것처럼 만들어주는 것을 말한다.

특정 값 리턴하기
@Test
void returnNull(@Mock UsersService usersService,
                @Mock EventRepository eventRepository) {

    Users user = new Users();
    user.setId(1L);
    user.setName("LEE");

    Users findUser = usersService.findById(1L);

    assertEquals("LEE", findUser.getName());
}

당연한 결과로 이 테스트는 실패한다.

usersService는 Null이다.
그런데 findById를 호출하기 때문에 NullPointerException이 발생한다.

@Test
void mockitoWhen(@Mock UsersService usersService,
                 @Mock EventRepository eventRepository) {

    Users user = new Users();
    user.setId(1L);
    user.setName("LEE");

    Mockito.when(usersService.findById(1L)).thenReturn(user);

    Users findUser = usersService.findById(1L);
    assertEquals("LEE", findUser.getName());
}

Mockito의 static 메서드인 when을 사용하면 결과가 달라진다.
userService.findById(1L) 을 호출할
user 객체를 반환하겠다는 의미이다.

그렇기때문에 이 테스트는 성공한다.

이 when 메서드 파라미터로 사용되는 메서드의 파라미터는 Matcher를 사용할 수 있다.

  • ArgumentMatchers.any() : 아무 값이나 받을 수 있다.
  • ArgumentMatchers.anyLong() : 아무 Long을 받을 수 있다.
  • ArgumentMatchers.anyString() : 아무 String을 받을 수 있다.


예외 던지기

void 메서드를 호출하면 아무 일도 발생하지 않는다.
만약 void 메서드를 통해 예외 발생을 기대한다면 어떻게 할까?

@Test
void mockitoDoThrow(@Mock UsersService usersService,
                    @Mock EventRepository eventRepository) {

    Users user = new Users();
    user.setId(1L);
    user.setName("LEE");

    Mockito.when(usersService.findById(ArgumentMatchers.any())).thenReturn(user);

    Users findUser = usersService.findById(1L);
    assertEquals("LEE", findUser.getName());

    Mockito.doThrow(new IllegalArgumentException()).when(usersService).duplicateCheck(2L);

    usersService.duplicateCheck(1L);     // 40번 라인
    usersService.duplicateCheck(2L);     // 41번 라인
}

//
java.lang.IllegalArgumentException
	at com.example.mockitodemo.mockito.service.EventServiceTest.createEventService(EventServiceTest.java:41)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:221)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

먼저 doThrow 메서드는 duplicateCheck 메서드의 파라미터에 2라는 값을 넣었을 때
IllegalArgumentException이 발생하도록 한다.

그렇기 때문에 40번 라인은 무사히 통과하고 41번 라인에서 예외가 발생한다.


같은 메서드가 여러번 호출될 때 마다 다른 행동하기
@Test
void manyTest(@Mock UsersService usersService,
              @Mock EventRepository eventRepository) {
    Users user = new Users();
    user.setId(1L);
    user.setName("LEE");

    Users newUser = new Users();
    newUser.setId(2L);
    newUser.setName("GICHEOL");

    when(usersService.findById(any()))
            .thenReturn(user)
            .thenThrow(new RuntimeException())
            .thenReturn(newUser);

    Users findUser = usersService.findById(1L);
    assertEquals("LEE", findUser.getName());

    assertThrows(RuntimeException.class, () -> usersService.findById(3L));

    assertEquals(newUser, usersService.findById(2L));
}

usersService.findById(아무 값)이 호출될 때 마다
어떤값을 리턴할지 지정해줄 수 있다.

위 예제의 경우
첫 번째로는 user 객체를,
두 번째로는 RuntimeException을
세 번째로는 newUser를 리턴한다.

미리 어떤 값을 리턴할지 지정한 것이기 때문에
사실 상 usersService.findById()에
정말 타입이 맞는 아무 값을 넣어도 모두 예정된 값이 나온다.

실수할 수도 있어보여 주의해서 사용해야 될 것 같다.


Tags:

Categories:

Updated:

Leave a comment