์ ๋ ์ปจํธ๋กค๋ฌ๋ฅผ ํ ์คํธ ํ ๋, MockMvc + RestDocs๋ฅผ ์ด์ฉํด์ ํ ์คํธ์ ๋ฌธ์ํ๋ฅผ ์งํํ๊ณ ์์ต๋๋ค.
(RestAssured์ ์์ง ์จ๋ณธ์ ์ด ์๊ณ , ์ธ์ ๊ฐ ๊ณต๋ถํด์ผ์ง ํ๊ณ ์ผ๋จ์ ๋ฏธ๋ค๋ ์ํ์ ๋๋น..ใ ใ )
์๋ฌดํผ MockMvc๋ฅผ ์ด์ฉํด์ ํ ์คํธ๋ฅผ ์งํํ๋๋ฐ, ์ ๋ง ๊ทธ๋ฐ๊ฑด์ง๋ ๋ชจ๋ฅด๊ฒ ์ง๋ง, ํด๋น ๊ณผ์ ์ด ์ ์ต์ํด์ง์ง๋ ์์ ๋ฟ๋๋ฌ, static import๊ฐ ๋๋ฌด ๋ง๊ณ , ๊ฐ์ ์ฝ๋์ ์ค๋ณต์ด ๋๋ฌด ๋ง์์ ธ์ ์กฐ๊ธ ๊ท์ฐฎ์ ํ๋ ์ํ์์ต๋๋ค.
๊ทธ๋์ ์ด๋ฅผ ๊ฐ์ ํ๊ณ ์, ๋ฉ์๋ ์ฒด์ด๋์ ์ ์ฉํ์ฌ ์กฐ๊ธ ์๊ฐ ์์ด? MockMvc ํ ์คํธ๋ฅผ ์งํํ ์ ์๋๋ก ๊ตฌํํด ๋ณด์๋๋ฐ์, ์ด๋ฌํ ๊ณผ์ ์ ๊ณต์ ํด๋ณด๊ณ ์ ๊ธ์ ์์ฑํฉ๋๋ค.
(์ฝ๋๊ฐ ์กฐ๊ธ ์ ์ด์ ์ ์๊ณ , ์ ๊ฐ ์ฃผ๋ก ์ฐ๋ ๊ธฐ๋ฅ๋ค์ ๋ํด์๋ง ๊ฐํธํ๋ฅผ ํ๊ณ ์ ํ์๊ธฐ ๋๋ฌธ์ ๊ธฐ๋ฅ์ด ์กฐ๊ธ ๋ถ์กฑํ ์ ์์ง๋ง, ์ด๋ฅผ ํ์ฅํ๊ธฐ๋ ์ด๋ ต์ง ์์ ๊ฒ์ ๋๋ค.)
๐ง ํ ์คํธ์ ์ผ๋ฐ์ ์ธ ๊ณผ์
์ ๋ MockMvc ํ ์คํธ์ฝ๋๋ฅผ ์์ฑํ ๋ ์ผ๋ฐ์ ์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ๊ณผ์ ์ ํตํด ์์ฑํฉ๋๋ค.
- mockMvc.perform(HttpMethod(URL, ๊ฒฝ๋ก ๋ณ์ ์ค์ ))
- header ์ค์ [์ ๋ ์ฃผ๋ก ์ธ์ฆ์ ์ํ AccessToken๋ง์ ์ค์ ํฉ๋๋ค]
- content ์ค์ [๋์ฒด๋ก JSON์ผ๋ก ์ค์ ํ๊ฑฐ๋, ํน์ ์ค์ ํ์ง ์์ต๋๋ค]
- ์์ํ๋ ๋ฐํ Status ์ค์
- ์ดํ ๋ฌธ์ํ ์์
์๋ฅผ ๋ค์ด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
@Test
void ์ธ์ฆ๋_์ฌ์ฉ์์_์ฌ๋ฐ๋ฅธ_๋ชจ์_์์ฑ_์์ฒญ์ธ_๊ฒฝ์ฐ_๋ชจ์์_์์ฑํ๊ณ _201์_๋ฐํํ๋ค() throws Exception {
final Long memberId = 1L;
setAuthentication(memberId); // ์ธ์ฆ ์ ๋ณด ์ธํ
ResultActions resultActions = mockMvc.perform(post(CREATE_CLUB_URL)
.header(HttpHeaders.AUTHORIZATION, BEARER_ACCESS_TOKEN)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(correctRequest))
).andExpect(status().isCreated());
resultActions.andDo(document(/* ๋ฌธ์ํ */));
}
@Test
void ๋์์_์ญํ _๋ณ๊ฒฝ_์ฑ๊ณต_์_200์_๋ฐํํ๋ค() throws Exception {
// given
doNothing().when(changeParticipantRoleUseCase).command(any());
setAuthentication(memberId);
// when
ResultActions resultActions = mockMvc.perform(
post(CHANGE_PARTICIPANT_ROLE_URL, clubId, participantId, clubRoleId)
.header(HttpHeaders.AUTHORIZATION, BEARER_ACCESS_TOKEN)
).andExpect(status().isOk());
// then
resultActions.andDo(document(/* ๋ฌธ์ํ */));
}
์ ๋ฐ๋ณต๋๋ ๊ณผ์ ๋ค์ด ์กฐ๊ธ ๊ท์ฐฎ๊ณ ๋ณต์กํ๋ค๊ณ ๋๊ปด์ ์ด๋ฅผ ๊ฐํธํ ํ๋ ค๊ณ ํฉ๋๋ค.
๐ง ์ฒด์ด๋์ ํตํ ๊ฐํธํ
๋ฐ๋ก ๊ฒฐ๊ณผ ์ฝ๋๋ถํฐ ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์์ง๋๋ค.
@Test
void ์ธ์ฆ๋_์ฌ์ฉ์์_์ฌ๋ฐ๋ฅธ_๋ชจ์_์์ฑ_์์ฒญ์ธ_๊ฒฝ์ฐ_๋ชจ์์_์์ฑํ๊ณ _201์_๋ฐํํ๋ค() throws Exception {
ResultActions resultActions = postRequest()
.url(CREATE_CLUB_URL)
.login()
.jsonContent(correctRequest)
.expectStatus(HttpStatus.CREATED);
resultActions.andDo(document(/* ๋ฌธ์ํ */));
}
@Test
void ๋์์_์ญํ _๋ณ๊ฒฝ_์ฑ๊ณต_์_200์_๋ฐํํ๋ค() throws Exception {
// given
doNothing().when(changeParticipantRoleUseCase).command(any());
// when
ResultActions resultActions = postRequest()
.url(CHANGE_PARTICIPANT_ROLE_URL, clubId, participantId, clubRoleId)
.login()
.noContent()
.expectStatus(OK);
// then
resultActions.andDo(document(/* ๋ฌธ์ํ */));
}
์ฌ์ค ์ด๋ง ๋ด์๋ ๋ณ ์ฐจ์ด๊ฐ ์๋ค๊ณ ๋๋ ์ ์์ง๋ง, ํด๋น ๋ฐฉ์์ ์ฅ์ ์ ๋ฉ์๋ ์ฒด์ด๋์ ํตํด, ๋ค์ ๊ณผ์ ์ ์กฐ๊ธ ์๊ฐ ์์ด? ๋ฐ๋ก๋ฐ๋ก ์งํํ ์ ์๋ค๋ ๊ฒ์ ์์ต๋๋ค.
์ด๋ฅผ ์ง์ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ณผ์ ์ผ๋ก ๋น๊ตํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๐ญ ๊ธฐ์กด ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ณผ์
ํด๋น ๋ฉ์๋๋ฅผ ์ธ์ฐ์ง ์์ผ๋ฉด get(), post() ๋ฑ์ ์ฐพ๋ ๊ฒ๋ถํฐ ํ๋ฒ ๋ฉ์นซํ๊ฒ๋ฉ๋๋ค.
import๋ฅผ ํด์ฃผ์ด์ผ ํฉ๋๋ค
์ดํ url์ ์ธํ ํฉ๋๋ค
perform์ ๊ดํธ ๋ง๊ณ , post()์ ๊ดํธ์ header() ์ฌ์ฉ ํ ์ธํ ํฉ๋๋ค.
json์ ์ํด conten์ contentType์ ์ ํด์ฃผ์ด์ผ ํฉ๋๋ค.
objectMapper๋ฅผ ํตํด ๋ณํ ๊ณผ์ ์ด ํ์ํฉ๋๋ค.
andExpect์์ ๋ ํ๋ฒ ๋ฉ์๋๋ฅผ ์ธ์ฐ์ง ์์ผ๋ฉด ๋ฉ์นซํฉ๋๋ค.
static import๋ฅผ ๋ ํ๋ฒ ํด์ฃผ์ด์ผ ํฉ๋๋ค.
์๋ฃ๋์์ต๋๋ค.
๐ ๊ฐํธํ
์์ฒญ ๋ณด๋ผ ๋ฉ์๋๋ก ์์ํ์ฌ, ์ดํ ๊ฐ๋ฅํ ๋ฉ์๋๊ฐ url์ ์ธํ ํด์ฃผ๋ ๋ฉ์๋๋ฐ์ ์์ต๋๋ค.
url ์ธํ ์ดํ์๋ ๋ก๊ทธ์ธ์ ์ฒ๋ฆฌํ ๊ฒ์ธ์ง ์ฒ๋ฆฌํ์ง ์์ ๊ฒ์ธ์ง ๊ฒฐ์ ํฉ๋๋ค.
์ดํ์๋ content๋ฅผ ๊ฒฐ์ ํ๊ฑฐ๋, ํน์ ๋ฐ๋ก ๊ฒฐ๊ณผ๋ก ๋์ด๊ฐ ์ ์์ต๋๋ค.
(ํ์ฌ json๋ง ์ค์ ํ ์ ์๋๋ก ๊ตฌํํ์์ง๋ง, ์ด๋ ๋ฉ์๋๋ฅผ ์ถ๊ฐํจ์ผ๋ก์จ ์ถฉ๋ถํ ํ์ฅ ๊ฐ๋ฅํฉ๋๋ค.)
๊ฒฐ๊ณผ๋ก๋ ์์ฃผ ์ฌ์ฉํ๋ ๊ฒ๋ค๋ง ์ค์ ํด ์ฃผ์์ต๋๋ค.
๋ ๋ฐฉ์์ ๋น๊ตํด ๋ดค์ ๋, ํ์คํ ๊ฐํธํด ์ง ๊ฒ์ ์ ์ ์์ต๋๋ค.
๋ํ ๋ฌด์๋ณด๋ค๋ ๋ฉ์๋๋ฅผ ์ธ์์ผํ๋ ๊ท์ฐฎ์๊ณผ, static import๋ฅผ ํด์ผํ๋ ๊ท์ฐฎ์์ด ์ฌ๋ผ์ก์ต๋๋ค :)
๐ง ๋๋ต์ ์ธ ๊ตฌ์กฐ
- RequestBuilder : ์ค์ ์ ํ์ํ ๋ชจ๋ ์ ๋ณด๋ค์ ์ ์ฅํฉ๋๋ค. ์๋ ๋จ๊ณ๋ค์ ์งํํ๋ฉฐ RequestBuilder๊ฐ ํ์๋ก ํ๋ ์ค์ ๋ค์ ์ธํ ํด๋๊ฐ๋๋ค.
- Method : ์์ฒญ์ ๋ณด๋ผ HttpMethod๋ฅผ ์ค์ ํ๋ ๋จ๊ณ์ ๋๋ค. ์ดํ EndPoint๋ก ์งํ๋ฉ๋๋ค.
- EndPoint : ์์ฒญ์ ๋ณด๋ผ URL์ ์ค์ ํ๋ ๋จ๊ณ์ ๋๋ค. ์ดํ Authentication์ผ๋ก ์งํ๋ฉ๋๋ค.
- Authentication : ๋ก๊ทธ์ธ ์ฌ๋ถ๋ฅผ ์ค์ ํ๋ ๋จ๊ณ์ ๋๋ค. ์ดํ Content๋ก ์งํ๋ฉ๋๋ค.
- Content : content๋ฅผ ์ค์ ํฉ๋๋ค. ์ดํ Expect๋ก ์งํ๋ฉ๋๋ค.
- Expect: ์์ฒญ์ ๋ํ ์์ ๊ฒฐ๊ณผ๋ฅผ ์ค์ ํ๋ ๋จ๊ณ์ ๋๋ค. ์ดํ ์ง๊ธ๊น์ง์ ์ค์ ์ ๋ฐํ์ผ๋ก MockMvc.perform์ ์ํํฉ๋๋ค.
๐ง ๊ตฌํ ๊ณผ์
ํด๋น ์ฝ๋๋ ์ ์ ํ๋ก์ ํธ์ ์กฐ๊ธ ์ต์ ํ๋์ด ์์ง๋ง, ๋๋ต์ ์ธ ๋๋๋ง ๊ฐ์ ธ๊ฐ๋ฉด ๋ค๋ฅธ ํ๋ก์ ํธ์์๋ ์ถฉ๋ถํ ์ฌ์ฉํ ์ ์๋ค๊ณ ์๊ฐํฉ๋๋ค.
public class RequestBuilder {
/* ์ธ์ฆ์ ์ํ AccessToken ๋ง๋๋ ์๋น์ค */
private CreateTokenUseCase createTokenUseCase = new CreateToken(new MockJwtProperties());
private MockMvc mockMvc;
private ObjectMapper objectMapper;
/* ์์ฒญ ๋ณด๋ผ url */
private String url;
/* ๊ฒฝ๋ก ๋ณ์๋ค */
private List<Object> urlVariables;
/* ์์ฒญ ๋ณด๋ผ HttpMethod */
private HttpMethod method;
/* header์ ์ค์ ํด์ค ๊ฐ */
private Map<String, Object> headers = new HashMap<>();
private MediaType contentType;
/* body์ ๋ด๊ธธ content */
private Object content;
/* ์์ ๋ฐํ status */
private HttpStatus status;
private RequestBuilder(final MockMvc mockMvc, final ObjectMapper mapper) {
this.mockMvc = mockMvc;
this.objectMapper = mapper;
}
/* ์์ฑ ์ดํ HttpMethod๋ฅผ ์ ์ํ๋๋ก ํฉ๋๋ค. */
public static Method request(final MockMvc mockMvc, final ObjectMapper objectMapper) {
return new RequestBuilder(mockMvc, objectMapper)
.request();
}
/* ์์ฑ๊ณผ ๋์์ HttpMethod๋ฅผ Get์ผ๋ก ์ค์ ํ ํ, url์ ์
๋ ฅ๋ฐ์ ์ ์๋๋ก ํฉ๋๋ค. */
public static EndPoint getRequest(final MockMvc mockMvc, final ObjectMapper objectMapper) {
return new RequestBuilder(mockMvc, objectMapper)
.request()
.get();
}
public static EndPoint postRequest(final MockMvc mockMvc, final ObjectMapper objectMapper) {
return new RequestBuilder(mockMvc, objectMapper)
.request()
.post();
}
public static EndPoint deleteRequest(final MockMvc mockMvc, final ObjectMapper objectMapper) {
return new RequestBuilder(mockMvc, objectMapper)
.request()
.delete();
}
private Method request() {
return new Method();
}
/* HttpMethod๋ฅผ ์ ํ๋ ๋จ๊ณ์
๋๋ค. */
public class Method {
public EndPoint method(final HttpMethod methodInput) {
method = methodInput;
return new EndPoint();
}
public EndPoint get() {
method = GET;
return new EndPoint();
}
public EndPoint post() {
method = POST;
return new EndPoint();
}
public EndPoint delete() {
method = DELETE;
return new EndPoint();
}
}
/* URL์ ์ ํ๋ ๋จ๊ณ์
๋๋ค */
public class EndPoint {
public Authentication url(final String urlInput, final Object... urlVariablesInput) {
url = urlInput;
urlVariables = Arrays.asList(urlVariablesInput);
return new Authentication();
}
}
/* ์ธ์ฆ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๋ ๋จ๊ณ์
๋๋ค. */
public class Authentication {
/* ๋ก๊ทธ์ธ ์, AccessToken์ ์๋ก ๋ฐ๊ธํ์ฌ Header์ ์ค์ ํฉ๋๋ค. */
public Content login() {
Claims claims = new Claims();
claims.addClaims(MEMBER_ID_CLAIM, "1");
AccessToken accessToken = new AccessToken(createTokenUseCase.command(new CreateTokenUseCase.Command(claims)));
headers.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.token());
return new Content();
}
public Content noLogin() {
return new Content();
}
}
/* Content Body๋ฅผ ์ธํ
ํฉ๋๋ค. */
public class Content {
/* JSON์ ์ธํ
ํฉ๋๋ค. */
public Expect jsonContent(final Object object) {
contentType = MediaType.APPLICATION_JSON;
content = object;
return new Expect();
}
/* ๋ค์ ๋จ๊ณ๋ก ์งํํฉ๋๋ค. */
public Expect expect() {
return new Expect();
}
/* Content ๊ด๋ จํด์ ์ถ๊ฐํ๊ณ ์ถ์ผ๋ฉด ์ด๊ณณ์ ๋ ์ถ๊ฐํ ์ ์์ต๋๋ค. */
}
/* ์์ ๊ฒฐ๊ณผ๋ฅผ ์ค์ ํฉ๋๋ค. */
public class Expect {
public ResultActions expectStatus(HttpStatus expectStatus) throws Exception {
status = expectStatus;
return makeRequest();
}
public ResultActions ok() throws Exception {
status = HttpStatus.OK;
return makeRequest();
}
public ResultActions created() throws Exception {
status = HttpStatus.CREATED;
return makeRequest();
}
public ResultActions unAuthorized() throws Exception {
status = HttpStatus.UNAUTHORIZED;
return makeRequest();
}
public ResultActions forbidden() throws Exception {
status = HttpStatus.FORBIDDEN;
return makeRequest();
}
public ResultActions badRequest() throws Exception {
status = HttpStatus.BAD_REQUEST;
return makeRequest();
}
public ResultActions notFound() throws Exception {
status = HttpStatus.NOT_FOUND;
return makeRequest();
}
public ResultActions conflict() throws Exception {
status = HttpStatus.CONFLICT;
return makeRequest();
}
}
/* ์ฒด์ด๋์ ๋ง์ง๋ง ๋จ๊ณ๋ก, ์ค์ ๊ฒฐ๊ณผ๋ฅผ ๋ง๋ค์ด๋
๋๋ค. */
private ResultActions makeRequest() throws Exception {
MockHttpServletRequestBuilder request = RestDocumentationRequestBuilders.request(method, url, urlVariables.toArray());
for (String header : headers.keySet()) {
request.header(header, headers.get(header));
}
if (content != null) {
request.contentType(contentType)
.content(objectMapper.writeValueAsString(content));
}
return mockMvc.perform(request)
.andExpect(status().is(status.value()));
}
}
์ ์ฝ๋๋ ๋จ์ง ์ ๊ฐ ํนํ ๋ง์ด ๋ฐ๋ณต๋์ด ์ฌ์ฉํ๋ ์ฝ๋๋ค์ ๊ฐํธํํ๊ธฐ์ํด ์์ฑํ ์ฝ๋์ด๋ฏ๋ก, ๋ง์ ๊ธฐ๋ฅ์ด ์์ต๋๋ค.
๊ทธ๋ฌ๋ ์๋ก์ด ๋จ๊ณ๋ฅผ ๋ง๋ค๊ฑฐ๋, ๊ธฐ์กด ๋จ๊ณ์ ๋ฉ์๋๋ฅผ ์ถ๊ฐํจ์ผ๋ก์จ ๊ธฐ๋ฅ ํ์ฅ์ด ์ด๋ ต์ง ์์ผ๋ฉฐ, ๋ง์ฝ ๊ธฐ๋ฅ ์ถ๊ฐ๊ฐ ์ด๋ ค์์ง๋ ๊ฒฝ์ฐ์๋ ๊ธฐ์กด์ ๋ฐฉ๋ฒ์ฒ๋ผ MockMvc๋ฅผ ๊ทธ๋ฅ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค :)
์ด์ Controller ํ ์คํธ๋ฅผ ์ํ ์ถ์ ํด๋์ค๋ฅผ ํ๋ ์์ฑํด ์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
@AutoConfigureRestDocs
public abstract class ControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
protected RequestBuilder.Method request() {
return RequestBuilder.request(mockMvc, objectMapper);
}
protected RequestBuilder.EndPoint getRequest() {
return RequestBuilder.getRequest(mockMvc, objectMapper);
}
protected RequestBuilder.EndPoint postRequest() {
return RequestBuilder.postRequest(mockMvc, objectMapper);
}
protected RequestBuilder.EndPoint deleteRequest() {
return RequestBuilder.deleteRequest(mockMvc, objectMapper);
}
}
์ด์ ์ปจํธ๋กค๋ฌ๋ฅผ ํ ์คํธํ๋ ์ฝ๋์์ ์ด๋ฅผ ์์๋ฐ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ฌ์ฉ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๋ด์ฃผ์ ์ ๊ฐ์ฌํ๊ณ , ํน์ ๊ฐ์ ์ ์ด ์๋ค๋ฉด ๋ง์ํด์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค.