WebTestClient는 실제로 HTTP 요청을 보낸다

강동훈·2022년 7월 3일
0

TIL

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를 JpaRepositoryfindById()를 통해 찾고, 존재하지 않을 경우 예외를 던진다.

테스트 실행 결과 해당 아이디를 갖는 대상이 존재하지 않는다며 예외가 발생했다. 분명 MockMvc를 통해 Spring WebMVC를 테스트할 때 작성해본 적이 있는 코드이고, 성공하는 코드인데 이상했다.

WebTestClient vs MockMvc

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

stackoverflow 본문

또한 @Trancsactional을 붙였기 때문에 테스트 코드가 완료되기 전까지 커밋되지 않고(DB에 저장되지 않고), 저장하는 쓰레드와 조회하는 쓰레드는 다르므로 조회하는 쓰레드의 1차 캐시에서도 저장한 데이터를 볼 수 없다. 이러한 이유로 테스트가 실패한 것이다.

해결 방법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)
}

해결 방법2 - 테스트 코드 수정

테스트하고자 했던 것은 리파지토리의 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)
            }
        }
}
profile
안녕하세요, 강동훈입니다.

0개의 댓글