spring boot에서 integration test를 작성할 때 RestTemplate
이나 WebClient
를 이용한 http call에 대한 테스트를 지원하는 여러가지 툴이 있는데,
그중에 WireMock
에 대한 사용법을 공유하고자 한다.
전체 코드는 아래에서 볼 수 있습니다.
https://github.com/pooi/wiremock-example
WireMock이란?
https://wiremock.org
WireMock은 지정한 로컬 포트에 mocking 서버를 띄워 해당 포트로 호출하는 http request에 대해 mocking 해서 원하는 응답 값을 내려주도록 도와주는 툴이다.
method, path, query params, headers를 모킹 조건으로 설정할 수 있으며,
모킹 조합에 맞는 request가 들어왔을때 적절한 응답 값 (200, 404, 500 등)과 body를 리턴하도록 세팅할 수 있다.
WireMock 시작하기
아래처럼 wiremock dependency를 gradle에 추가해준다.
testImplementation("com.github.tomakehurst:wiremock-standalone:2.27.2")
아래와 같이 기본적인 Specification을 작성한다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class WebClientSpec extends Specification {
@Shared
ObjectMapper objectMapper = new ObjectMapper()
@ClassRule
@Shared
WireMockRule server = new WireMockRule(10001)
@Autowired
TestClient testClient
def setupSpec() {
server.start()
}
def cleanup() {
assert !server.findUnmatchedRequests().requests
server.resetAll()
}
def cleanupSpec() {
server.stop()
}
...
}
testClient
는 본 프로젝트에서 WebClient를 통해 http request를 전송하는 역할을 한다.new WireMockRule(10001)
을 통해 10001번 포트에 mock 서버를 띄운다.setupSpec
에서 모든 테스트 시작전에 wire mock server를 시작시킨다.cleanupSpec
에서는 모든 테스트가 완료된 후 wire mock server를 종료시킨다.cleanup
에서는assert !server.findUnmatchedRequests().requests
를 통해 매 테스트가 끝난 후 미처 확인하지 못한 request가 남아있는지 체크한다. (spock에서0 * _
의 역할과 같다.)
WireMock 사용하기
단순 Path에 대한 Mocking 및 검증
def "get"() {
given:
def expectedResult = [
test: "123"
]
server.addStubMapping(
get(urlPathEqualTo("/main"))
.willReturn(okJson(objectMapper.writeValueAsString(expectedResult)))
.build()
)
when:
def result = CoroutineTestUtils.executeSuspendFun {
testClient.callWithResponse(Map, HttpMethod.GET, "/main", null, null, it)
} as Map<String, String>
then:
result == expectedResult
server.verify(1, getRequestedFor(urlPathEqualTo("/main")))
0 * _
}
testClient가 GET /main
을 호출했을때 expectedResult
가 정상적으로 리턴되는지 확인하는 테스트이다.
given:
에서 http request에 대한 mocking을 선언한다.
server.addStubMapping
에서 get(...)을 통해 GET 메서드에 대한 mocking임을 설정한다.urlPathEqualTo
를 통해/main
path에 대한 mocking임을 설정한다.willReturn
을 통해 http request가 위 조건에 매칭 될 경우 지정한 response를 반환하도록 설정한다.
urlPathEqualTo와 urlEqualTo의 차이
urlPathEqualTo
는 말 그대로 path에 대한 일치만 확인하고,urlEqualTo
는 path + query param까지 일치하는지 확인한다./main?test=test1
일 경우urlPathEqualTo
는/main
만 확인하고,urlEqualTo
는/main?test=test1
인지 확인한다.
then:
에서 결과를 검증한다.
result == expectedResult
http request를 통해 반환된 결과가 기대했던 결과와 일치한 지 확인한다.- wire mock은 mocking과 일치한 request가 들어왔으면 willReturn에 선언한 response를 반환하지만, 단 하나라도 조건과 일치하지 않으면 stubbing에러와 함께 willReturn에 선언한 response를 반환하지 않는다.
server.verify(1, getRequestedFor(urlPathEqualTo("/main")))
를 통해 해당 모킹 서버에/main
path로 요청이 1번 왔는지 검증한다.getRequestedFor
,postRequestedFor
,deleteRequestedFor
등 http method 별로 검증하는 방법을 제공한다.
Query Params에 대한 Mocking 및 검증
query param을 mocking 조건에 추가하도 싶다면 아래 3가지 방법을 선택하면 된다.
urlEqualTo
사용하기
def "get with query params - 1"() {
given:
def expectedResult = [
test: "123"
]
def queryParams = [
"test": ["test1"]
]
server.addStubMapping(
get(urlEqualTo("/main?test=test1"))
.willReturn(okJson(objectMapper.writeValueAsString(expectedResult)))
.build()
)
when:
def result = CoroutineTestUtils.executeSuspendFun {
testClient.callWithResponse(Map, HttpMethod.GET, "/main", queryParams, null, it)
} as Map<String, String>
then:
result == expectedResult
server.verify(1, getRequestedFor(urlEqualTo("/main?test=test1")))
0 * _
}
urlPathEqualTo
가 아닌 urlEqualTo
를 사용하면 query param에 대한 조건까지 모킹에 사용 가능하다.
이제 wire mock server는 query param에 대한 내용까지 일치해야지만 지정한 response를 반환할 것이다.
다만, url에 대한 하드 코딩 방식의 조건이기 때문에 query param의 key가 여러 개일 경우 순서를 보장할 수 없을 수 있다는 단점이 존재한다.
따라서 아래의 방식으로 좀 더 정형화된 mocking이 가능하다.
withQueryParam
사용하기
def "get with query params - 2"() {
given:
def expectedResult = [
test: "123"
]
def queryParams = [
"test": ["test1"]
]
server.addStubMapping(
get(urlPathEqualTo("/main"))
.withQueryParam("test", equalTo("test1"))
.willReturn(okJson(objectMapper.writeValueAsString(expectedResult)))
.build()
)
when:
def result = CoroutineTestUtils.executeSuspendFun {
testClient.callWithResponse(Map, HttpMethod.GET, "/main", queryParams, null, it)
} as Map<String, String>
then:
result == expectedResult
server.verify(
1,
getRequestedFor(urlPathEqualTo("/main"))
.withQueryParam("test", equalTo("test1"))
)
0 * _
}
urlPathEqualTo
로 path에 대해서만 체크하고, query param은 .withQueryParam("test", equalTo("test1"))
를 통해 체크하는 방식이다.
여러 key에 대한 조건을 줄 때는 withQueryParam
을 여러번 사용하면 된다.
하지만, 이 방식도 단점이 존재한다. 아시다시피 query param의 value는 리스트를 지원한다.
만약 query param이 "test": ["test1", "test2", "test3"]
이렇고, value 리스트에 대한 순서를 보장할 수 없는 경우라면
위와 같은 방식은 또 한 번 한계를 나타낸다. 따라서 아래와 같은 방식으로 또 한번 보완할 수 있다.
andMatching
사용하기
def "get with query params - 3"() {
given:
def expectedResult = [
test: "123"
]
def queryParams = [
"test": ["test1", "test2", "test3"]
]
def mappingBuilder = get(urlPathEqualTo("/main"))
.willReturn(okJson(objectMapper.writeValueAsString(expectedResult)))
queryParams.entrySet().each {
mappingBuilder.andMatching {request ->
MatchResult.of(
Matchers.containsInAnyOrder(it.value as String[])
.matches(request.queryParameter(it.getKey()).values())
)
}
}
server.addStubMapping(mappingBuilder.build())
...
}
andMatching
과 Matchers.containsInAnyOrder
를 통해 순서와 관계없이 query param의 key와 value list가 모두 일치하는지 체크할 수 있다.
Headers에 대한 Mocking 및 검증
def "get with headers"() {
given:
def expectedResult = [
test: "123"
]
def headers = [
"test": ["test1"]
]
server.addStubMapping(
get(urlPathEqualTo("/main"))
.withHeader("test", equalTo("test1"))
.willReturn(okJson(objectMapper.writeValueAsString(expectedResult)))
.build()
)
when:
def result = CoroutineTestUtils.executeSuspendFun {
testClient.callWithResponse(Map, HttpMethod.GET, "/main", null, headers, it)
} as Map<String, String>
then:
result == expectedResult
server.verify(
1,
getRequestedFor(urlPathEqualTo("/main"))
.withHeader("test", equalTo("test1"))
)
0 * _
}
header에 대한 검증은 query param과 유사하게 .withHeader
를 이용하면 된다.
Request Body에 대한 Mocking 및 검증
def "post with body"() {
given:
def body = [
"example1": "test1",
"example2": "test2"
]
server.addStubMapping(
post(urlPathEqualTo("/main"))
.withRequestBody(equalToJson(toJson(
[
"example1": "test1"
]
), true, true))
.build()
)
when:
CoroutineTestUtils.executeSuspendFun {
testClient.callWithBody(HttpMethod.POST, "/main", null, null, body, it)
}
then:
server.verify(1, postRequestedFor(urlPathEqualTo("/main")))
0 * _
}
request body가 정확히 일치하는지 혹은 부분적으로 일치하는지에 대한 체크가 가능하다.
.withRequestBody(equalToJson(String value, boolean ignoreArrayOrder, boolean ignoreExtraElements))
를 이용하면 된다.
ignoreArrayOrder
, ignoreExtraElements
의 이름에서 보다시피 설정하는 방식에 따라 list의 순서까지 체크할 것인지와 body의 field 중 선언하지 않은 field에 대해서도 체크할 것인지 지정할 수 있다.
예시에서는 request body는 example1
과 example2
2개의 field가 존재하지만, mocking에서는 example1
field에 대해서만 체크하고 있다. example2
의 값뿐만 아니라 추가적으로 들어간 field에 대해선 don't care 하겠다는 의미이다.
여러 가지 에러 Response에 대해 반환하기
@Unroll
def "error - #status"() {
given:
server.addStubMapping(
post(urlPathEqualTo("/main"))
.willReturn(response)
.build()
)
when:
CoroutineTestUtils.executeSuspendFun {
testClient.callWithoutResponse(HttpMethod.POST, "/main", null, null, it)
}
then:
def exception = thrown(WebClientResponseException)
exception.rawStatusCode == status
server.verify(1, postRequestedFor(urlPathEqualTo("/main")))
0 * _
where:
response || status
badRequest() || 400
unauthorized() || 401
forbidden() || 403
notFound() || 404
serverError() || 500
status(422) || 422
}
지금까지는 willReturn
에서 okJson
(200 OK + response body)을 반환했지만, 위 예시처럼 여러가지 응답 값을 설정할 수 있다.
각각의 에러 이름을 통한 지정이 가능하고, 또한 status(code)
를 통해 특정한 status code를 반환할 수도 있다.
Error Response와 Error Body도 반환하기
def "error with body"() {
given:
def errorBody = [
code: 404,
message: "Device not found"
]
server.addStubMapping(
post(urlPathEqualTo("/main"))
.willReturn(badRequest().withBody(toJson(errorBody)))
.build()
)
when:
CoroutineTestUtils.executeSuspendFun {
testClient.callWithoutResponse(HttpMethod.POST, "/main", null, null, it)
}
then:
def exception = thrown(WebClientResponseException)
exception.rawStatusCode == 400
exception.responseBodyAsString == toJson(errorBody)
server.verify(1, postRequestedFor(urlPathEqualTo("/main")))
0 * _
}
.withBody(...)
를 사용하면 에러에 대해 body를 포함한 response를 반환할 수 있다.
'Server > Test' 카테고리의 다른 글
Spock 병렬 테스트하기 (0) | 2022.10.23 |
---|---|
Spring Boot 테스트 속도 개선기 (0) | 2022.09.19 |