MockMvc
를 활용해 Controller
에 대한 슬라이스 테스트를 작성한다면, 주로 다음과 같이 작성할 것입니다.
mockMvc.perform(post("/lines/{lineId}/sections", 1L)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpectAll(
status().isCreated(),
jsonPath("$.stationResponses[0].id", is(upStation.getId()), Long.class),
jsonPath("$.stationResponses[0].name", is(upStation.getName())),
jsonPath("$.stationResponses[1].id", is(downStation.getId()), Long.class),
jsonPath("$.stationResponses[1].name", is(downStation.getName()))
);
이 때, 검증에 사용되는 ResultActions(andExpect(), andExpectAll())
에 대해 간단히 살펴보고자 합니다.
문서에서 ResultActions
는 다음과 같이 설명하고 있습니다.
실행된 요청에 대해, 기대하고 있는 결과 혹은 동작을 적용할 수 있습니다.
즉 mockMvc
를 통해 요청을 수행한 결과를 관리하는 클래스임을 알 수 있습니다.
ResultActions
에서 제공해주는 기능을 통해, 요청의 결과를 활용할 수 있다고 이해할 수 있겠습니다.
기대하고 있는 내용을 수행
할 수 있는 메소드입니다.
해당 메소드의 가장 큰 특징은 자기 자신인 ResultActions
를 반환한다는 점입니다.
이를 통해 다음과 같은 Chaining Pattern
을 적용할 수 있습니다.
mockMvc.perform(post("/lines/{lineId}/sections", 1L)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.stationResponses[0].id", is(upStation.getId()), Long.class))
.andExpect(jsonPath("$.stationResponses[0].name", is(upStation.getName())))
.andExpect(jsonPath("$.stationResponses[1].id", is(downStation.getId()), Long.class))
.andExpect(jsonPath("$.stationResponses[1].name", is(downStation.getName())));
다만, 주의할 점이 하나 필요합니다.
mockMvc.perform(post("/lines/{lineId}/sections", 1L)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadGateway())
.andExpect(jsonPath("$.stationResponses[0].id", is(-999L), Long.class))
.andExpect(jsonPath("$.stationResponses[0].name", is("wrong name")))
.andExpect(jsonPath("$.stationResponses[1].id", is(-999L), Long.class))
.andExpect(jsonPath("$.stationResponses[1].name", is("wrong name")));
위와 같이, andExpect()
로 지정한 모든 조건들이 false
가 나오도록 변경했습니다.
테스트 결과는 어떤 식으로 나올까요?
5가지 테스트가 모두 틀렸지만, 상태 코드, status()
에 대한 값만이 검증되는 것을 확인할 수 있습니다.
즉, assertThat()
의 문제를 그대로 가지고 있다고 볼 수 있습니다.
이럴 때 사용할 수 있는 것이 andExpectAll()
메소드 입니다.
문서에서도 This feature is similar to the SoftAssertions support in AssertJ and the assertAll() support in JUnit Jupiter.
, 즉 assertAll()
과 유사하다고 적혀 있습니다.
mockMvc.perform(post("/lines/{lineId}/sections", 1L)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpectAll(
status().isBadGateway(),
jsonPath("$.stationResponses[0].id", is(-999L), Long.class),
jsonPath("$.stationResponses[0].name", is("wrong name")),
jsonPath("$.stationResponses[1].id", is(-999L), Long.class),
jsonPath("$.stationResponses[1].name", is("wrong name"))
);
이전과 동일하게 5개의 항목 모두 실패하는 테스트를 작성하고 실행하면 다음과 같이 동작함을 확인할 수 있습니다.
andExpect(), andExpectAll()
모두 ResultMatcher
를 파라미터로 받는 것을 확인하셨을 겁니다.
ResultMatcher
는 인터페이스로, 요청에 의한 결과가 지정한 예상 값과 일치(match
)하는지 확인하는 용도입니다.
match()
메소드를 구현하면 되며, 일치하지 않을 시 Exception
을 던지도록 되어 있습니다.
다만 우리는 이를 직접 구현할 필요가 없습니다.
스프링이 MockMvcResultMatchers
라는 이름의 Factory Class
를 제공해주기 때문입니다.
설명부터 ResultMatcher
기반의 Factory Method
를 지원한다는 내용을 확인할 수 있습니다.
코드를 직접 확인해보면, 다음과 같은 내용을 확인할 수 있습니다.
그 항목을 간략하게 정리하면 다음과 같습니다.
메소드 | 설명 |
---|---|
content() | 응답 본문의 내용을 검증합니다. |
header() | 응답 헤더의 내용을 검증합니다. |
status() | 응답 상태 코드를 검증합니다. |
jsonPath() | JSON 응답 본문의 특정 부분을 검증합니다. |
xpath() | XML 응답 본문의 특정 부분을 검증합니다. |
view() | 선택된 뷰를 검증합니다. |
flash() | 플래시 속성을 검증합니다. |
forwardedUrl() | 요청이 전달된 URL을 검증합니다. |
redirectedUrl() | 요청이 리디렉션된 URL을 검증합니다. |
model() | 모델 속성을 검증합니다. |
handler() | 요청을 처리한 핸들러를 검증합니다. |
다양한 메소드를 제공해주는 것을 확인할 수 있는데, 이 중 자주 사용했던 status()
와 jsonPath()
를 확인해보도록 하겠습니다.
StatusResultMatchers
는 status()
를 호출하면 반환하는 값으로, HTTP 상태 코드
를 검증하는데 사용합니다.
특이 사항으로는, 다양한 HTTP 상태 코드
를 검증하는 메소드를 제공해준다는 점입니다.
그 항목을 간략하게 정리하면 다음과 같습니다.
메소드 | 설명 |
---|---|
is1xxInformational() | 1xx 범위의 응답 상태 코드가 있는지 확인합니다. |
is2xxSuccessful() | 2xx 범위의 응답 상태 코드가 있는지 확인합니다. |
is3xxRedirection() | 3xx 범위의 응답 상태 코드가 있는지 확인합니다. |
is4xxClientError() | 4xx 범위의 응답 상태 코드가 있는지 확인합니다. |
is5xxServerError() | 5xx 범위의 응답 상태 코드가 있는지 확인합니다. |
is(int status) | 지정된 정수 값과 동일한 응답 상태 코드가 있는지 확인합니다. |
is(HttpStatus status) | 지정된 HTTP 상태 코드와 동일한 응답 상태 코드가 있는지 확인합니다. |
isOk() | 200 OK 상태 코드가 있는지 확인합니다. |
isCreated() | 201 Created 상태 코드가 있는지 확인합니다. |
isNoContent() | 204 No Content 상태 코드가 있는지 확인합니다. |
isNotFound() | 404 Not Found 상태 코드가 있는지 확인합니다. |
isMethodNotAllowed() | 405 Method Not Allowed 상태 코드가 있는지 확인합니다. |
isInternalServerError() | 500 Internal Server Error 상태 코드가 있는지 확인합니다. |
상태 코드의 범위를 확인할 수도 있고, 특정 상태 코드를 명시할 수도 있으니 적절하게 사용하면 될 것 같습니다.
JsonPathResultMatchers
는 jsonPath()
를 호출하면 반환하는 값으로, 표현식을 통해 응답에 접근하고 검증할 수 있습니다.
json
형식에 접근하기 위한 수단입니다.
다음과 같은 기능을 통해 다양한 방식으로 json
에 접근할 수 있습니다.
글자 | 설명 |
---|---|
$ | 현재 노드를 의미합니다. |
* | 모든 자식 필드에 대해 접근합니다. |
. | 자식 필드에 대해 접근합니다. |
[] | 배열 요소에 대해 접근합니다. |
@ | 속성에 대해 접근합니다. |
예를 들어, 다음과 같이 사용할 수 있습니다.
// users라는 배열 중 첫 번째 인덱스의 name 속성
jsonPath("$.users[0].name")
// user라는 객체의 name 속성
jsonPath("$.user.@name")
jsonPath()
는 일반적으로 사용하는 jsonPath(String, Matcher)
클래스와,
오버로딩이 적용된 jsonPath(String, Matcher, Class)
가 있습니다.
문서를 확인해보면, This can be useful for matching numbers reliably — for example, to coerce an integer into a double.
, 이 기능은 Integer를 Double로 강제 캐스팅하는 등 숫자를 안정적으로 일치시키는 데 유용할 수 있습니다.
라고 설명하고 있습니다.
내부 코드를 확인해보면, 검증에 실패하는 경우
를 지정한 값이 다를 경우 뿐만 아니라, 지정한 타입으로 캐스팅하지 못한 경우에도 AssertionError
, 검증이 실패함을 확인할 수 있습니다.
즉 잘못된 타입 캐스팅을 할 경우에 별도의 예외 처리가 필요 없다는 것입니다.
예시를 통해 확인해보겠습니다.
mockMvc.perform(post("/lines")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpectAll(
status().isCreated(),
jsonPath("$.id", is(line.getId())),
jsonPath("$.name", is(line.getName())),
jsonPath("$.color", is(line.getColor()))
);
이 테스트는 다음과 같이 실패합니다.
이를 성공시키기 위해서는, jsonPath()
의 세 번째 인자를 통해 타입을 강제로 캐스팅해줄 수 있습니다.
mockMvc.perform(post("/lines")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpectAll(
status().isCreated(),
jsonPath("$.id", is(line.getId()), Long.class),
jsonPath("$.name", is(line.getName())),
jsonPath("$.color", is(line.getColor()))
);