spring / / 2023. 11. 15. 08:28

Mockito 라이브러리

자바로 테스트를 작성하다 보면 모든 경우를 실제로 동작하는 테스트로 만들면 좋겠지만, 테스트 하기 힘든 환경, 시간이 너무 오래 걸리는 경우, 네트워크 연결이 실패하는 경우 등 다양한 이유 때문에 이 부분은 된다고 치고 필요한 부분만 테스트를 하는 경우가 있다.

이런 경우에 사용할 수 있는 라이브러리가 Mockito이다. Mock은 가짜 객체 의미로 껍데기 객체를 만들어 호출할 때 실패나지 않게 만들 수 있는 것이다.

준비

의존성

spring-boot를 사용하는 경우는 spring-boot-starter-test를 통해 자동으로 mockito가 포함되지만 그렇지 않은 경우는 의존성을 추가해줘야 한다.

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <scope>test</scope>
</dependency>

mock 사용 준비

그리고 테스트 클래스에 MockitoJUnitRunner를 붙여주자. (JUnit 5)

@ExtendWith(MockitoExtension.class)
public class MockitoAnnotationUnitTest {
    ...
}

만일 JUnit 4를 사용한다면 아래와 같이 붙여주면 된다.

@RunWith(MockitoJUnitRunner.class)
public class MockitoAnnotationUnitTest {
    ...
}

혹은 테스트 시작 전에 아래와 같이 추가할 수도 있다. (JUnit 5)

@BeforeEach
public void init() {
    MockitoAnnotations.openMocks(this);
}

JUnit 4라면

@Before
public void init() {
    MockitoAnnotations.openMocks(this);
}

Mockito 어노테이션

Mockito에서 사용하는 어노테이션의 의미를 알아보자.

  • @Mock
    • 실제 인스턴스를 만드는 것이 아니라 대상 클래스를 가지고 가상의 mock 객체를 만드는 것
  • @Spy
    • 실제 인스턴스를 래핑(wrapping)해서 spy 객체를 만드는 것. 특정 행위를 지정하지 않으면 실제 객체와 동일하게 동작하고 지정된 행위에 대해서만 스터빙(stub)을 함
  • @InjectMocks
    • 클래스 내에 목(mock) 객체를 주입하기 위한 용도
  • @MockBean
    • 스프링의 ApplicationContext에 mock 객체를 추가하기 위한 용도
  • @SpyBean
    • 스프링의 ApplicationContext에 spy 객체를 추가하기 위한 용도

@Mock

Mockito에서 가장 많이 사용되는 어노테이션은 @Mock이다. Mockito.mock을 사용하지 않고 @Mock을 통해서 인스턴스를 생성하고 주입할 수 있다.

@Mock
List<String> mockedList;

@Test
public void mockTest() {
    mockedList.add("one"); // mock 객체(mockedList)에 추가
    assertEquals(0, mockedList.size());  // mock이라 크키가 1이 아니고 0이다.

    when(mockedList.size()).thenReturn(100);
    assertEquals(100, mockedList.size());
}

@Spy

다음은 @Spy 어노테이션을 사용하여 list을 spy하는 예제이다.

@Spy
List<String> spyList = new ArrayList<>();

@Test
public void spyTest() {
    spyList.add("홍길동");  // 실제 객체(spyList)에 추가
    assertEquals(1, spyList.size());  // 실제 객체와 동일하다.

    doReturn(100).when(spyList).size(); // 특정 행위가 있으면 mock처럼 동작한다.
    Assert.assertTrue(spyList.size() == 100); // spy 객체의 크기 조회
}
  • spyList에 항목을 추가하는데 spyList.add()는 실제 메소드가 사용되었다.
  • Mockito.doReturn() 을 사용하여 1 대신 100을 리턴하도록 spiedList.size() 메소드를 스텁(stub)하였다.

위의 테스트 예제를 실행해보면 Java 17 이상에서 오류가 발생할 수 있다.

이런 경우 Java 버전을 낮추거나 아래와 같이 inline mockmaker로 전환하는 방법을 사용할 수 있다.

inline mockmaker로 전환하는 방법
  1. test/resource에 mockito-extensions 디렉토리 생성
  2. mockito-extensions 하위에 org.mockito.plugins.MockMaker 파일 생성
  3. org.mockito.plugins.MockMaker 파일을 열고 mock-maker-inline 텍스트를 추가
  4. 끝~, 다시 실행해보면 잘 된다.



Mock과 Spy의 차이

Mockito는 목(mock)을 생성할 때, 실제 인스턴스가 아닌 클래스에서 생성한다. mock은 단지 클래스의 껍데기만 가지고 있는 인스턴스를 만든다.

반면에 spy는 기존 인스턴스를 감싸고 있는 객체를 만든다. 그래서 기존 객체와 동일하게 동작한다.

@Spy
List<String> spyList = new ArrayList<>();

@Mock
List<String> mockList;

@Test
public void mockSpyTest() {
    mockList.add("mock 추가");
    spyList.add("spy 추가");

    assertTrue(mockedList.size() == 0); // mock은 크기가 0
    assertTrue(spyList.size() == 1); // spy는 크기가 1
}

@InjectMocks

테스트 객체에 자동으로 목(mock) 필드를 주입하기 위해 @InjectMocks를 사용해보자.

다음 예제는 WordMap 목을 MyDictionary dic에 주입하도록 @InjectMocks를 사용한다.

@Mock
Map<String, String> wordMap;

@InjectMocks
MyDictionary dic = new MyDictionary();

@Test
public void whenUseInjectMocksAnnotation_thenCorrect() {
    when(wordMap.get("aWord")).thenReturn("aMeaning");

    assertEquals("aMeaning", dic.getMeaning("aWord"));
}

다음은 MyDictory 클래스이다.

public class MyDictionary {
    Map<String, String> wordMap;

    public MyDictionary() {
        wordMap = new HashMap<String, String>();
    }
    public void add(final String word, final String meaning) {
        wordMap.put(word, meaning);
    }
    public String getMeaning(final String word) {
        return wordMap.get(word);
    }
}

@MockBean, @SpyBean

테스트를 위해 아래 샘플 코드를 작성하였다.

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserStore userStore;

    public void register(User user) {
        boolean successDB = userStore.saveToDB(user); // DB에 저장
        boolean successElastic = userStore.saveToElasticsearch(user); // elasticsearch에 저장
    }
}
@Component
@Slf4j
public class UserStore {

    public boolean saveToDB(User user) {
        log.info("DB에 실제 저장");
        return true;
    }

    public boolean saveToElasticsearch(User user) {
          log.info("Elasticsearch에 실제 저장");
          return true;
    }
}

위의 register 메소드를 SpringBoot에서 테스트로 작성하면 아래와 같이 된다.

@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Autowired
    private UserStore userStore;

    @Test
    public void register() {
        userService.register(new User(1, "홍길동"));
    }
}

이를 실행하면 아래와 같다.

[           main] com.example.user.store.UserStore         : DB에 실제 저장
[           main] com.example.user.store.UserStore         : Elasticsearch에 실제 저장

@MockBean

위의 예제에서 userStore는 DB와 elasticsearch에 저장하는 코드인데, 네트웍이 연결되지 않아 DB와 elasticsearch 테스트를 하고 싶지 않을 수도 있다. 이런 경우는 아래와 같이 @MockBean을 사용하면 된다.

@MockBean
private UserStore userStore;

이를 실행하면 실제 내부의 메소드가 실행되지 않는다. (로그가 찍히지 않음)

@SpyBean

만일 userStore에서 DB는 실제로 저장을 하고 Elasticsearch는 mock으로 처리를 하고 싶은 경우도 있을 것이다.

userStore를 @MockBean으로 하면 내부 메소드가 모두 mock이 되지만 Spy는 일부의 메소드만 mock으로 처리하고 싶은 경우에 사용한다.

이럴 때는 UserStore를 @SpyBean을 추가하고 mock으로 처리하고 싶은 경우만 doReturn을 하면 된다.

@SpyBean
private UserStore userStore;

@Test
public void registerWithSpy() {
    doReturn(true).when(userStore)
            .saveToElasticsearch(any(User.class)); // elasticsearch 저장 시만 mock으로 처리

    userService.register(new User(1, "홍길동"));
}

실행하면 아래 로그가 남는다. (elasticsearch 저장 로그는 남지 않음)

[           main] com.example.user.store.UserStore         : DB에 실제 저장

@Mock 대신 @MockBean으로 테스트하면 편한 경우

아래와 같이 BlogService가 있다.

@Service
@RequiredArgsConstructor
public class BlogService {

    private final PostRepository postRepository;

    private final CommentRepository commentRepository;

    private final ReplyRepository replyRepository;

    public void register() {
        postRepository.register();
        commentRepository.register();
        replyRepository.register();
    }
}

blog 등록(register)시 postRepository, commentRepository, replyRepository 3개에 저장을 하는 로직이 있다고 하자.

commentRepository는 외부 서비스를 restTemplate를 호출을 하는 코드가 있다. 이 URL은 외부 서비스라 테스트 대상에서 제외한다.

@Component
@RequiredArgsConstructor
public class CommentRepository {

    private final RestTemplate restTemplate;

    public void register() {
        restTemplate.postForEntity("http://blog-register-url", String.class, String.class);
        System.out.println("comment 등록");
    }
}

그리고 BlogService를 테스트 코드를 작성해보자.

case 1: 실제 service 호출
@SpringBootTest
public class BlogServiceTest {

    @Autowired
    private BlogService blogService;

    @Test
    public void register() {
        blogService.register();
    }
}
실행결과
post 등록

org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://blog-register-url": blog-register-url;

이렇게 되면 commentRepository를 실제로 호출을 하므로 안된다.

case 2: @Mock으로 처리

그래서 @Mock으로 필요한 부분에 추가하자. @Mock을 주입할 수 있게 blogService에 @InjectMocks를 추가하였다.

@SpringBootTest
public class BlogServiceTest {

    @InjectMocks
    private BlogService blogService;

    @Mock
    private PostRepository postRepository;

    @Mock
    private CommentRepository commentRepository;

    @Mock
    private ReplyRepository replyRepository;
}
실행결과

모든 repository 빈이 목(mock)으로 실행된다.

이렇게 하면 테스트는 성공하지만 우리가 원하는 결과가 아니다. 단지 commentRepository만 mock으로 처리해야 한다.

case 3: @MockBean으로 처리

이럴 때는 실제 목(mock)으로 처리할 빈에 @MockBean만 달면 나머지 빈들은 실제 동작을 하게 된다.

@SpringBootTest
public class BlogServiceTest {

    @Autowired
    private BlogService blogService;

    @MockBean
    private CommentRepository commentRepository;
}
실행결과
post 등록
reply 등록

참고

https://www.baeldung.com/mockito-annotations

https://www.baeldung.com/mockito-spy

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유