티스토리 뷰

 
클래스 간 의존관계가 없는 자바 어플리케이션에서는 JUnit만으로도 테스트가 가능하다. 그러나 스프링의 핵심 개념인 DI(Dependency Injection), 즉 의존관계 주입으로 인해 각 클래스 간 의존관계가 생겨 테스트가 힘들어지게 된다. 대표적인 예를 들어보자. 스프링으로 웹 개발을 할 때 일반적으로 Controller - Service - Repository 계층을 구성하며 Controller는 Service를, 다시 Service는 Repository를 의존하게 된다. 이때 Service 계층을 테스트하려고 테스트 코드를 작성하고 실행 버튼을 눌렀더니 테스트에 실패했다. 비즈니스 로직은 아무리 봐도 잘못된 것이 없는데... 아뿔싸 Service가 의존하고 있던 Repository 계층에 잘못된 코드가 있었던 것이다.


여기서 문제는 Service 계층의 테스트가 실패한 이유가 Service 클래스가 아닌 Repository 클래스 때문이라는 점이다. 우리가 하고자 하는 테스트는 단위 테스트(Unit Test)이다. 단위 테스트는 '컴퓨터 프로그래밍에서 소스 코드특정 모듈이 의도된 대로 정확히 작동하는지 검증하는 절차' 라고 정의되는데, 여기서 중요한 것은 특정 모듈이다. 즉 Service 계층의 테스트이면 Service만 제대로 동작하는지 검증하면 되지, Repository의 동작까지 검증할 필요도 없고, 해서도 안된다. 만약 Repository 계층에 문제가 있다면 그건 Repository 계층 테스트에서 검증을 수행하면 된다.
 
그럼 어떻게 계층 간의 의존관계를 끊어내고 각 클래스를 독립적으로 테스트할 수 있을까? 이를 위해 Mockito 라는 오픈 소스 프레임워크를 사용할 수 있다. Mockito가 하는 역할을 간단히 말하자면 Service 클래스를 테스트할 때 실제 Repository 객체가 아닌, 꼭두각시 같은 가짜 Repository 객체를 주입해주는 것이다. 이 주입된 Repository 객체는 정말 꼭두각시 같기 때문에 '이렇게 동작해라' 라고 명령할 수 있어, Service 클래스만을 독립적으로 테스트할 수 있게 된다. 그럼 Mockito를 적용하고 이를 JUnit과 함께 사용해 테스트 코드를 작성하는 방법에 대해 자세히 알아보자. 
 


 

What is a Mock?

 

 

Mockingjay 라는 부제가 달린 헝거게임 시리즈가 있다. 이 mockingjay는 다른 새들의 노랫소리를 흉내내는 새인 흉내지빠귀(mockingbird)와 전쟁 중에 적들의 계획을 알아내기 위해 만들어낸, 일종의 살아있는 녹음기 역할을 하는 재잘어치(jabberjay)가 짝짓기를 해서 만들어진 헝거게임 세계관 내의 새라고 한다. 


Mockito가 만들어낸 mock 객체도 mockingjay와 거의 비슷하게 동작한다. 즉 mock 객체는 실제 객체를 흉내내며 mock 객체에 대해 행해지는 것들을 기억할 수 있다. 그럼 Mockito가 만들어내는 mock 객체란 무엇이며, 실제 객체와는 어떤 차이가 있을까?
 


먼저 실제 객체는 an actual instance of a class 라고 정의되며, 실제 객체의 메소드 호출은 실제 클래스에 정의되어 있는 메소드 바디를 실행한다. 한마디로 그냥 우리가 알고 있는 일반적인 객체 인스턴스이다. 반면 mock 객체는 proxy interface 라고 정의되며, mock 객체의 메소드 호출은 실제 메소드 바디를 실행하지 않고 기본적으로 아무것도 하지 않는다. 따라서 mock 객체의 메소드 호출에 대한 동작은 별도로 정의해주어야 한다. (이 동작 정의 방법은 뒤에서 알아볼 것이다.) 추가적으로 mock 객체의 실제 객체와의 가장 큰 차이점은 다른 객체와의 interaction tracking이 가능하다는 것이다. 이게 무슨 말인가 하면 간단히 말해서 mock 객체의 특정 메소드가 몇번 호출되었는지, 특정 메소드가 호출되었을 때 어떤 파라미터가 전달되었는지 등을 자기가 기억할 수 있다는 것이다. 즉 실제 객체에 비해 기억력이 좋은 객체구나... 쯤으로 이해하면 된다. 아무튼 mock 객체가 기억하고 있는 내용들은 테스트 검증 대상이 된다.

mock 객체는 일반적으로 테스트될 수 없는 의존성(ex. 데이터베이스, 네트워크 관련)을 숨기기 위해 사용된다. (위에서 보았던 예제의 Service 내에서의 Repository에 대한 의존성처럼) 따라서 일반적으로 실제 테스트 대상인 클래스는 실제 객체로, 테스트될 클래스가 의존하고 있는 클래스는 mock 객체로 생성한다. 그럼 어떻게 Mockito를 적용하여 테스트될 실제 객체와 실제 객체가 의존하고 있는 mock 객체를 생성할 수 있는지 알아보자. 
 

 


스프링 부트로 개발을 한다면 기본적으로 spring-boot-starter-test 의존성이 포함되어 있을 것이고, 이 경우 JUnit5와 Mockito 라이브러리 또한 자동으로 추가된다. 따라서 스프링 부트에서는 별도의 설정 없이도 Mockito를 사용할 수 있다. 참고로 build.gradle에 mockito-core 라이브러리를 직접 추가해줘도 된다. 

dependencies {
    testImplementation 'org.mockito:mockito-core:3.+'
}

 

spring-boot-starter-test 의존성

 

 

이렇게 mockito 라이브러리를 추가하면 본격적으로 mock 객체를 생성할 준비가 된 것이다. 앞으로의 예제에서 사용할 Hello 클래스는 다음과 같다.

public class Hello {

    public void helloPrint() {
        System.out.println("hello world!");
    }

    public String helloReturn() {
        return "hello world!";
    }
}

 
 

 

@Mock

사실 mock 객체를 생성하는 방법에는 크게 3가지가 있다. 다음은 가장 기본적인 방법으로 직접 Mockito 클래스의 static 메소드를 통해 지정한 클래스의 mock 객체를 생성할 수 있다. 실제 생성된 mock 객체는 말그대로 실제 객체가 아닌 Hello 클래스의 객체인 척하는 가짜 객체이므로 helloPrint(), helloReturn() 메소드 호출 시 제대로 동작하지 않는 것을 확인할 수 있다. 

public class MockitoTest {

    @Test
    void mockTest() {
        Hello mock = Mockito.mock(Hello.class); //mock 객체 생성
        mock.helloPrint(); //출력X
        String str = mock.helloReturn(); //반환X
        assertThat(str).isNull();
    }
}

 
 
다음으로는 @Mock 어노테이션을 이용하는 방법으로 이 경우에는 @BeforeEach 메소드 내에서 MockAnnotations.openMock() 메소드를 호출해주어야 한다. MockAnnotations.openMock() 메소드는 현재 테스트 클래스 내에 Mockito 어노테이션(@Mock, @Spy, @Captor, @InjectMocks)이 선언된 필드들을 초기화해준다. 참고로 이 openMock() 초기화 메소드를 호출해주지 않으면 아래 mock 객체는 null 값을 가진다.

public class MockitoTest {

    @Mock
    Hello mock;

    @BeforeEach
    void init() {
        MockitoAnnotations.openMocks(this); //mock 객체 초기화
    }

    @Test
    void mockTest() {
        mock.helloPrint(); //출력X
        String str = mock.helloReturn(); //반환X
        assertThat(str).isNull();
    }
}

 


마지막으로 JUnit5와 함께 Mockito를 사용하는 경우 테스트 클래스 위에 @ExtendWith(MockitoExtention.class) 어노테이션을 선언해주면 JUnit의 테스트 사이클에 맞춰 Mockito가 알아서 mock 객체를 생성해서 주입해준다. 즉 앞서 수동으로 진행했던 초기화 과정이 자동화된 것이다. 반복적인 mock 생성 코드를 제거해주며 가독성도 더 좋기 때문에 공식 문서에서도 이 방법을 권장한다. @ExtendWith(MockitoExtention.class) 어노테이션은 JUnit에서 Mockito 관련 어노테이션을 사용할 수 있도록 해주는 확장 포인트라 생각하면 된다.

@ExtendWith(MockitoExtension.class)
class MockitoTest {

    @Mock
    Hello mock;

    @Test
    void mockTest() {
        mock.helloPrint(); //출력X
        String str = mock.helloReturn(); //반환X
        assertThat(str).isNull();
    }
}

 


자 이제 mock 객체를 생성했으면 이를 사용해야 한다. mock 객체는 꼭두각시처럼 무엇을 하라고 시키지 않으연 정말 아무것도 하지 않는다. mock 객체가 특정 메소드 호출 시 특정 동작을 하기를 원한다면 이를 지정해줄 수 있으며, 실제 해당 메소드를 호출하면 해당 동작을 수행한다.

또한 mock 객체는 생성 이후의 all interactions을 기억하고 있다. 즉 자신의 메소드 호출 내역들을 내부에 저장하고 있어, 예를 들어 특정 메소드가 몇번 호출되었는지, 특정 메소드가 호출될 때 해당 파라미터가 전달되었는지 등을 검증할 수 있게 해준다. 아래 코드로 이를 확인해보자. 
 
Mockitowhen(...).thenReturn(...) 메소드는 말 그대로 mock 객체의 helloReturn() 메소드가 호출되었을 때 "hi~" 문자열이 반환되도록 지정한다. 그리고 실제로 mock.helloReturn()를 호출한 결과 "hi~"가 반환된 것을 확인할 수 있다.


더불어 mock 객체의 메소드 호출 여부와 횟수도 검증할 수 있다. Mockito.verify(...) 메소드에 mock 객체와 검증할 내용을 전달하고 메소드 체이닝으로 검증할 mock 객체의 메소드의 이름을 작성하면 된다. 아래 코드에서는 helloReturn() 메소드는 한 번, helloPrint() 메소드는 아예 호출되지 않았음을 검증하고 있다.

@ExtendWith(MockitoExtension.class)
class MockitoTest {

    @Mock
    Hello mock;

    @Test
    void mockTest() {
        //given
        Mockito.when(mock.helloReturn()).thenReturn("hi~");

        //when
        String str = mock.helloReturn();

        //then
        assertThat(str).isEqualTo("hi~");
        Mockito.verify(mock, Mockito.times(1)).helloReturn();
        Mockito.verify(mock, Mockito.times(0)).helloPrint();
    }
}

 

참고로 mock 객체의 특정 메소드 호출 시 반환값을 지정한 것을 Stub이라고 한다. (실제 when(...).thenReturn(...) 메소드는 OngoingStubbing 구현체를 반환한다.) Stub에서는 메소드 호출 시의 반환값이나 발생하는 예외만을 지정할 수 있지 메소드 바디 자체를 오버라이딩처럼 완전히 재정의할 수는 없다. 따라서 Mockito.when(...)의 인자로 반환형이 void인 메소드 호출은 불가능하다. 즉 Mockito.when(mock.helloPrint())는 안된다는 것이다.

 


Mockito 사용 예제를 하나 더 보고 가자.

아래와 같이 HashMap<Integer, String> 타입의 mock 객체를 생성했는데, 이 객체의 get 메소드를 호출했을 때 "hello"를 반환하도록 지정했다. 이때 Stub에서 get 메소드의 인자로 Mockito.anyInt()를 지정하여 어떠한 int형 정수가 전달되더라도 "hello"가 반환된다. 아래 then 부분에서는 get(1) 메소드 호출은 1번, get(Mockito.anyInt()) 즉 int형 정수가 파라미터로 전달된 호출은 2번 발생했음을 검증한다. 
(물론 Mockito 클래스를 static import하여 코드를 더 간단하게 작성할 수 있다.)

@ExtendWith(MockitoExtension.class)
class MockitoTest {

    @Mock
    HashMap<Integer, String> mockHashMap;

    @Test
    void mockTest() {
        //given
        Mockito.when(mockHashMap.get(Mockito.anyInt())).thenReturn("hello");

        //when
        String str = mockHashMap.get(1);
        String str2 = mockHashMap.get(5);

        //then
        assertThat(str).isEqualTo("hello");
        assertThat(str2).isEqualTo("hello");
        Mockito.verify(mockHashMap, Mockito.times(1)).get(1);
        Mockito.verify(mockHashMap, Mockito.times(2)).get(Mockito.anyInt());
        //Mockito.verify(mockHashMap, Mockito.times(1)).get(2); //테스트 실패!
    }
}

 


추가적으로 Stub에서 지정한 메소드를 현재 테스트 내에서 실제로 호출하지 않으면 UnnecessaryStubbingException 예외가 발생하는데 이 경우에는 사용되지 않는 Stub을 제거하거나 lenient를 사용하라고 친절하게 알려준다. (왜 굳이 예외를 발생시키는지는 모르겠지만...) 아무튼 if문 같은 분기에 의해 해당 메소드가 호출될 수도 호출되지 않을 수도 있다면 Mockito.lenient().when(mockHashMap.get(Mockito.anyInt())).thenReturn("hello") 이렇게 작성해주면 된다.

 

 


@Mock 어노테이션 외에도 Mockito는 다양한 어노테이션을 제공한다. 이 어노테이션 또한 테스트 클래스에 @ExtendWith(MockitoExtension.class) 어노테이션이 선언되어 있으면 JUnit의 테스트 사이클에 맞춰 해당 객체가 자동으로 생성되고 주입된다. @Spy, @Captor, @InjectMocks 어노테이션이 있는데 하나씩 살펴보자.

 

 

@Spy

사실 Mockito가 생성하는 mock 객체에도 여러 종류가 존재한다. 앞서 @Mock 어노테이션에 의해 생성되는 mock 객체는 실제 객체를 흉내내는 가짜 객체인 반면, 이번에 소개할 @Spy 어노테이션에 의해 생성되는 mock 객체는 다른 특성을 지닌다. (혼동을 줄이기 위해 앞으로 @Spy에 의해 생성되는 객체는 spy 객체라고 지칭하겠다.)


spy 객체 또한 mock 객체와 동일하게 when(...).thenReturn(...)으로 특정 메소드 호출 시의 반환값 등을 지정할 수 있으며, 메소드 호출 여부 등 또한 추적이 가능하다. mock 객체와 다른 점 하나는 spy 객체는 real object라는 것이다. 즉 new 생성자로 생성된 일반적인 객체이면서도 mock 객체의 역할 또한 제공한다고 생각하면 된다. (실제로 mock 객체 생성 시에는 해당 클래스의 생성자가 호출되지 않는 반면 spy 객체는 new 생성자가 호출된다.)

 

proxy 개념을 잘 아는 분이라면 spy 객체를 proxy 객체라고 생각해도 무방하다. 즉 proxy처럼 내부 target으로 실제 객체를 두고 있으면서 부가 기능으로 mock 객체의 역할을 제공하는 것이다. 이름이 Spy인 이유도 실제 객체를 염탐하고 감시하기 때문이 아닐까...? 생각한다. 
 

@ExtendWith(MockitoExtension.class)
class MockitoTest {

    @Spy
    Hello spy;
    
    @Mock
    Hello mock

    @Test
    void spyTest() {
        //실제 객체 호출
        spy.helloPrint(); //hello world! 화면에 출력
        String str = spy.helloReturn();

        assertThat(str).isEqualTo("hello world!");
    }
    
    @Test
    void mockTest() {
        mock.helloPrint(); //nothing
        String str = mock.helloReturn(); //nothing
        
        assertThat(str).isNull();
    }
}

따라서 spy 객체는 when(...).thenReturn(...)으로 Stub을 지정하지 않더라도 메소드를 호출하면 실제 클래스에 정의된 메소드 바디 내용이 실행된다. 위 코드를 보면 spy 객체와 mock 객체의 확연한 차이를 느낄 수 있을 것이다. 어떤 블로그에서는 @Mock@Spy의 차이를 설명할 때 @Mock이 만드는 객체는 껍데기 객체(bare-bones shell instance), @Spy가 만드는 객체는 해당 클래스의 프록시 객체(proxies a real instance of the class)라 설명하기도 했다.
 

 

@ExtendWith(MockitoExtension.class)
class MockitoTest {

    @Spy
    Hello spy;

    @Test
    void spyTest() {
        //given
        Mockito.when(spy.helloReturn()).thenReturn("hi");

        //when
        String str = spy.helloReturn();

        //then
        assertThat(str).isEqualTo("hi");
        Mockito.verify(spy, Mockito.times(1)).helloReturn();
        Mockito.verify(spy, Mockito.times(0)).helloPrint();
    }
}

spy 객체도 when(...).thenReturn(...)으로 특정 메소드의 반환값을 지정해줄 수 있다고 했다. 위의 경우 helloReturn() 메소드 호출 시 실제 클래스에 정의된 메소드 바디가 실행되는 것이 아니라 지정한 Stub이 동작하여 "hi"를 반환하게 된다. 또한 mock 객체와 동일하게 helloReturn(), helloPrint() 메소드 호출 여부도 검증할 수 있다.

 


 

@Captor

@CaptorArgumentCaptor 객체를 생성하기 위해 사용되는 어노테이션이다. 그럼 이 ArgumentCaptor는 무언인가? 말그대로 메소드로 전달되는 인자(argument)를 capture, 즉 획득해주는 것이다. 이 어노테이션은 일반적으로 mock 객체의 메소드 호출 시 전달된 메소드 인자 값을 검증하기 위해 사용된다. 어떻게 사용되는지는 다음 코드를 보자. 
 

@ExtendWith(MockitoExtension.class)
class MockitoTest {

    @Mock
    HashMap<Integer, String> mockHashMap;

    @Captor
    ArgumentCaptor<Integer> keyCaptor;

    @Captor
    ArgumentCaptor<String> valueCaptor;

    @Test
    void captorTest() {
        mockHashMap.put(1, "hello");
        Mockito.verify(mockHashMap, Mockito.times(1)).put(keyCaptor.capture(), valueCaptor.capture());
        assertThat(keyCaptor.getValue()).isEqualTo(1);
        assertThat(valueCaptor.getValue()).isEqualTo("hello");

        mockHashMap.put(2, "hi");
        Mockito.verify(mockHashMap, Mockito.times(2)).put(keyCaptor.capture(), valueCaptor.capture());
        assertThat(keyCaptor.getValue()).isEqualTo(2);
        assertThat(valueCaptor.getValue()).isEqualTo("hi");
    }
}

@Captor 어노테이션이 선언된 ArgumentCaptor 객체 필드에는 어떤 타입의 값을 포착할 것인지 제네릭으로 지정해 주어야 한다. mockHashMap 객체의 put 메소드 호출 시 전달된 1과 "hello" 인자값을 포착하기 위해 각각 ArgumentCaptor<Integer> 타입의 keyCaptor와, ArgumentCaptor<String> 타입의 valueCaptor 객체를 사용했다. 메소드 호출 여부를 검증하는 verify 메소드에서 인자값을 획득할 수 있으며, 이렇게 획득된 메소드 인자값은 getValue 메소드로 반환받아 이를 Assertions로 검증할 수도 있다.
 
 

 

@InjectMocks

글의 초반에서 Mockito를 사용하는 가장 큰 이유는 의존관계가 있는 클래스들을 독립적으로 테스트하기 위해서라고 했다. 그럼 테스트할 클래스의 실제 객체를 생성하고 해당 객체가 의존하고 있는 객체는 @Mock 어노테이션으로 생성 후 직접 주입해주면 되나? 생각할 수 있지만 Mockito는 이러한 일련의 과정을 자동으로 수행해주는 어노테이션 또한 제공해준다. 
 
Mockito는 @InjectMocks 어노테이션이 붙은 필드는 실제 객체로 생성하고 만약 해당 클래스 내부에 의존관계가 있다면 이를 mock 객체 또는 spy 객체로 생성해서 자동으로 주입해준다. 이때 의존관계를 주입하는 방법에는 생성자 주입, setter 주입, 필드 주입이 있다. 
 

@Getter
@RequiredArgsConstructor //의존관계 생성자 주입
public class Service {

    private final Repository repository;

    public String doBusinessLogic(String id) {
        //Some Business Logic
        try {
            return repository.accessDataBase(id);
        } catch (Exception e) {
            return "exception!";
        }
    }
}

public class Repository {

    public String accessDataBase(String id) {
        if (Objects.equals(id, "ex")) throw new IllegalStateException();
        return "data";
    }
}

@ExtendWith(MockitoExtension.class)
class MockitoTest {

    @InjectMocks
    Service service;

    @Mock
    Repository repository; //생성된 repository 객체는 service 객체에 주입

    @Test
    void injectMocksTest() {
        System.out.println("repository = " + repository.hashCode()); //1936375962
        System.out.println("service.getRepository().hashCode() = " + service.getRepository().hashCode()); //1936375962
    }
}

위 코드를 보면 Service 클래스 내부에 Repository 클래스에 대한 의존관계가 존재한다. Service 클래스만을 독립적으로 테스트하기 위해서는 의존관계에 있는 Repository는 실제 객체가 아닌 mock 객체로 주입되어야 한다. 


테스트 클래스 내부에서 Service 필드에는 @InjectMocks 어노테이션을, Repository 필드에는 @Mock 어노테이션을 선언해주면 Mockito는 Respository 타입의 mock 객체를 생성하고, 이 Repository 타입의 mock 객체를 주입해 Service 타입의 실제 객체를 생성해준다. 실제 service 객체에 주입된 repository의 hashcode를 출력해보면 테스트 클래스의 @Mock 어노테이션이 붙은 repository의 값과 동일한 것을 확인할 수 있다. 
  

 

 

잘못된 테스트 

class ServiceTest {

    Service service;
    Repository repository;

    @BeforeEach
    void init() {
        repository = new Repository();
        service = new Service(repository);
    }

    @Test
    void incorrectTest() {
        String result = service.doBusinessLogic("someId");
        String result2 = service.doBusinessLogic("ex");

        assertThat(result).isEqualTo("data");
        assertThat(result2).isEqualTo("exception!");
    }
 }

위의 테스트는 Mokito를 사용하지 않았기 때문에 service, repository 필드 모두 실제 객체이다. 현재 테스트하고 있는 내용을 살펴보면 "someId"가 전달되었을 때에는 "data"가, "ex"가 전달되었을 때에는 "exception!"이 반환되는 것을 테스트하고 있다. 그러나 이는 잘못된 테스트이다.


사실 service가 수행하는 로직은 예외가 발생하지 않으면 repository의 반환값을 그대로 반환, 예외가 발생했을 때에는 "exception!"을 반환하는 것이다. 이때 문제는 service는 respotory의 accessDataBase 메소드가 어떤 값을 반환하는지 모르고, 어떤 경우에 accessDataBase 메소드에서 예외가 발생하는지도 모르는데 이를 모두 ServiceTest 클래스에서 테스트하고 있다는 것이다. 즉 ServiceTest에서 Service 뿐만 아니라 Repository까지 함께 테스트하게 된 것이다. 단위 테스트에서 이 둘은 철저히 분리해서 테스트할 필요가 있기 때문에 Mockito를 사용해야 한다. 
 
 

 

올바른 테스트

@ExtendWith(MockitoExtension.class)
class ServiceTest {

    @InjectMocks
    Service service;

    @Mock
    Repository repository = new Repository();

    @Test
    void correctTest() {
        //given
        Mockito.when(repository.accessDataBase(Mockito.anyString())).thenReturn("DATA");
        //when
        String result = service.doBusinessLogic("someId");
        //then
        assertThat(result).isEqualTo("DATA");
        Mockito.verify(repository, Mockito.times(1)).accessDataBase(Mockito.anyString());
    }

    @Test
    void correctTestWithException() {
        //given
        Mockito.when(repository.accessDataBase(Mockito.anyString())).thenThrow(new IllegalStateException());
        //when
        String result = service.doBusinessLogic("someId");
        //then
        assertThat(result).isEqualTo("exception!");
        Mockito.verify(repository, Mockito.times(1)).accessDataBase(Mockito.anyString());
    }
}

위 코드에서는 Mockito를 통해 service는 실제 객체를, repository는 mock 객체를 주입받아 테스트를 실행한다. 먼저 service는 doBusinessLogic 메소드에서 예외가 발생하지 않으면 repository가 반환한 값을 그대로 반환하다고 했다. correctTest는 이 로직을 테스트하고 있는데, 먼저 when(...).thenReturn(...)으로 repository의 accessDataBase 메소드에서 예외가 발생하지 않고 "DATA"가 반환되도록 직접 지정했기 때문에 그 아래에서는 service가 해당 로직을 잘 수행하는지 독립적으로 테스트할 수 있게 되었다. (현재 테스트에서는 repository의 accessDataBase 메소드가 어떤 값을 반환하는지 알고 있기 때문)
 


다음으로 service는 doBusinessLogic 메소드에서 예외가 발생하면 "exception!"을 반환한다. correctTestWithException는 이 로직을 테스트하고 있는데 앞선 테스트와 마찬가지로 repository의 accessDataBase 메소드를 호출했을 때 예외를 발생시키도록 Stub을 지정해주었다. 즉 service의 doBusinessLogic 메소드를 호출하면 항상 내부에서 예외가 발생함이 보장된 것이다. 따라서 예외가 발생했을 때 doBusinessLogic 메소드는 "exception!"을 반환함을 독립적으로 테스트할 수 있게 되었다. 
 

 

class RepositoryTest {

    Repository repository;
    
    @BeforeEach
    void init() {
        repository = new Repository();
    }

    @Test
    void correctTest() {
        String result = repository.accessDataBase("someId");
        assertThat(result).isEqualTo("data");

        assertThatThrownBy(() -> repository.accessDataBase("ex"))
                .isInstanceOf(IllegalStateException.class);
    }
}

더 완벽한 테스트를 위해서는 Repository를 별도의 테스트 클래스에서 테스트해야 한다. Repository의 주요 로직은 인자로 전달되는 id 값이 "ex"이면 IllegalStateException 예외를 발생시키고, 그렇지 않다면 "data"를 반환한다. correctTest에서 해당 로직을 테스트하고 있다. 

 

 

 

 

Mockito 어노테이션은 보통 Service 계층을 테스트할 때 사용된다. 그 이유는 일단 Controller는 HTTP 스펙에 묶여있어 실제 HTTP 요청이 발생해야 스프링의 HandlerMapping에 의해 컨트롤러가 매핑되기 때문이다. 즉 별도의 테스트 방법이 필요하다. 또한 Repository는 DB와 통신하는 계층으로 테스트가 어렵기도 하고 단위 테스트보다는 쿼리문의 성능 테스트를 위해 주로 통합 테스트를 수행하는 것으로 알고 있다.

 

다음 글에서는 간단하면서도 좀 더 의미있는 비즈니스 로직을 만들어보고 Mockito를 이용해 Service 계층을 직접 테스트 해보고자 한다. 테스트 작성 중 더 자세한 Mockito 관련 내용은 링크 참고
 
 
 
끝.
 
 
 

참고

https://howtodoinjava.com/mockito/mockito-annotations/
https://howtodoinjava.com/mockito/mockito-mock-injectmocks/
https://howtodoinjava.com/mockito/junit-mockito-example/

'자바 > 테스트' 카테고리의 다른 글

[Mockito] Spring Boot에서 Service 계층 테스트하기  (0) 2023.10.08
[JUnit] Parameterized Test  (0) 2023.06.24
[JUnit] AssertJ Assertions  (0) 2023.06.23
[JUnit] JUnit5 Assertions  (0) 2023.06.20
[JUnit] 테스트 Tagging과 Filtering  (0) 2023.06.02
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함