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()))
);