WebTestClient
는 실제로 HTTP 요청을 보낸다.
조회하는 API에 대한 테스트 코드를 다음과 같이 작성했다.
@DisplayName("메모 조회")
@Transactional // 테스트 데이터가 남지 않도록 롤백
@Test
fun getMemo() {
// 테스트를 위해 대상을 저장
val newMemo = Memo("test title", "test contents")
val savedMemo = memoRepository.save(newMemo)
// 저장한 대상 조회
webTestClient.get().uri("/memos/${savedMemo.id}")
.exchange()
.expectStatus().isOk
.expectBody<Memo>()
.consumeWith { exchangeResult ->
exchangeResult.responseBody?.let {
assertEquals(it, savedMemo)
}
}
}
실행마다 성공하는 테스트를 보장하기 위해 조회할 대상을 저장한 후에 조회를 수행하는 순서로 코드를 작성했다. 참고로 /memos/{id}
로 요청을 보내면 해당 id를 JpaRepository
의 findById()
를 통해 찾고, 존재하지 않을 경우 예외를 던진다.
테스트 실행 결과 해당 아이디를 갖는 대상이 존재하지 않는다며 예외가 발생했다. 분명 MockMvc
를 통해 Spring WebMVC를 테스트할 때 작성해본 적이 있는 코드이고, 성공하는 코드인데 이상했다.
WebTestClient
는 실제 HTTP request를 보내서 저장하는 코드(테스트 코드)와 조회하는 코드가 다른 쓰레드에서 수행되고, MockMvc
는 요청을 mocking하여 저장하는 코드와 조회하는 코드가 같은 쓰레드에서 수행된다고 한다.
When using the TestWebClient (or the TestRestTemplate) you are actually issuing a real HTTP request to your started server.
...
When using MockMvc you are replacing the actual container with a mocked instance and it uses a MockHttpServletRequest etc. and executes in the same thread
또한 @Trancsactional
을 붙였기 때문에 테스트 코드가 완료되기 전까지 커밋되지 않고(DB에 저장되지 않고), 저장하는 쓰레드와 조회하는 쓰레드는 다르므로 조회하는 쓰레드의 1차 캐시에서도 저장한 데이터를 볼 수 없다. 이러한 이유로 테스트가 실패한 것이다.
@Transactional
을 떼고 테스트가 끝날 때 테스트를 위해 저장했던 데이터를 제거하여 원상태로 복구시킨다.
@DisplayName("메모 조회")
@Test
fun getMemo() {
val newMemo = Memo("test title", "test contents")
val savedMemo = memoRepository.save(newMemo)
webTestClient.get().uri("/memos/${savedMemo.id}")
.exchange()
.expectStatus().isOk
.expectBody<Memo>()
.consumeWith { exchangeResult ->
exchangeResult.responseBody?.let {
assertEquals(it, savedMemo)
}
}
memoRepository.delete(savedMemo)
}
테스트하고자 했던 것은 리파지토리의 save()
와 findById()
가 아니다. 이 두 메소드는 직접 구현한 코드가 아니라 Spring Data JPA가 구현해준 것이므로 굳이 검증하지 않아도 되며, 설령 검증을 하고 싶다고 하더라도 컨트롤러 테스트에서 하는 건 그다지 바람직하지 않은 것 같다.
리파지토리 계층을 모킹하여 컨트롤러만 테스트할 수 있게, 조회 API가 기대한 대로 동작하는지 테스트할 수 있게 다음과 같이 수정했다. (사실 리파지토리 메소드를 호출하는 것 외에 아무 코드도 없긴하다.)
@DisplayName("메모 조회")
@Test
fun getMemo() {
val memo = Memo(1, "test title", "test contents")
given(memoRepository.findById(memo.id!!)).willReturn(Optional.of(memo))
webTestClient.get().uri("/memos/${memo.id}")
.exchange()
.expectStatus().isOk
.expectBody<Memo>()
.consumeWith { exchangeResult ->
exchangeResult.responseBody?.let {
assertEquals(it, memo)
}
}
}