이번 포스팅은 Spring RestDocs를 프로젝트에 적용하고 간단하게 테스트만 해볼 것 이다. 사실 적용만 하면 어려울 건 없기에 최대한 간단하고 빠르게 Spring RestDocs를 적용하는 법을 알아보도록 하자. (생각보다 복잡해서 내가 안까먹을려고 적는거긴 하다.)
참고로 설정파일에서 건드릴건 딱히 없다.
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
해당 포스팅에선 2.0.6 릴리즈 버전을 사용한다.
plugins {
...
id "org.asciidoctor.jvm.convert" version "3.3.2" // Spring Boot RestDocs
}
configurations {
...
asciidoctorExt // Spring Boot RestDocs
}
ext {
snippetsDir = file('build/generated-snippets')
}
test {
outputs.dir snippetsDir
}
asciidoctor {
inputs.dir snippetsDir
configurations 'asciidoctorExt'
sources{
include("**/index.adoc")
}
baseDirFollowsSourceFile()
dependsOn test
}
bootJar {
dependsOn asciidoctor
from("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
위와 같은 내용을 gradle에 추가해줄건데, asciidoctor의 문서 생성을 제어하고 빌드할때 배포되는 방식이다. 적어놓은대로 패키지를 따라야하고 조금이라도 어긋나면 문서가 생성되지 않거나 오류가 날 수 있으니 패키지를 잘 구성해야 한다.
이미지의 패키지 구조와 동일하게 설정 후 templates 안에 request와 response의 field의 형식을 지정해줄 것 이다.
==== Request Fields
|===
|Path|Type|Optional|Description
{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{#optional}}O{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/fields}}
|===
asciidoctor의 문법으로 테이블을 구성하는 템플릿이라고 생각하면 된다. Response의 경우 맨 상단의 이름만 Request Fields -> Response Fields로 바꿔주면 된다.
main 패키지에서 resoucres 하위에 static 이라는 패키지를 만들어준다. build를 통해 jar 파일을 만들때 해당 패키지를 통해 파일을 만들 것 이기 때문! 안은 비워놔도 좋지만 배포할때 패키지안에 파일이 없으면 패키지가 만들어지지 않는 경우가 있으니 README.MD 같은 파일을 만들어놔도 좋다 😉
사실 이 부분은 기존에 컨트롤러에 관한 테스트 코드가 있다면, 그대로 적용해도 좋지만 request와 response 값이 많고 docs를 만들어내는 코드가 워낙 많아질 수 있기에 테스트 코드와 docs 생성을 위한 테스트 코드는 분리해두는 것이 좋다.
테스트코드를 작성하는 패키지에서 docs라는 패키지를 추가적으로 만들고 RestDocsSupport라는 abstract class를 만들어 줄 것 이다. 참고로 RestDocsExceptionTest는 필자가 Exception을 문서로 생성하려할때 임의로 만든거여서 꼭 있을 필요는 없다.
@ExtendWith(RestDocumentationExtension.class)
public abstract class RestDocsSupport {
protected MockMvc mockMvc;
protected ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
void setUp(RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.standaloneSetup(initController())
.apply(documentationConfiguration(provider))
.build();
}
protected abstract Object initController();
}
해당 클래스는 RestDocs를 만들기 위해 필요한 설정들을 해놓은 추상 클래스이다. 기존 컨트롤러 테스트를 그대로 가져올 것 이기 때문에 사실 이미 검증된 테스트를 docs 로 만들어준다. 그러니까 성능을 위해 실제 객체를 사용하지 않고 Mocking 하여 테스트들을 docs로 만들어줄 것 이다.
이제 실제 테스트를 작성해보자. 일단 이전에 작성해둔 이메일을 검증하는 컨트롤러 테스트를 하나 긁어와봤다.
class UserControllerDocsTest extends RestDocsSupport {
private final UserService userService = mock(UserService.class);
@Override
protected Object initController() {
return new UserController(userService);
}
...
}
restdocs를 생성하기위한 테스트 클래스를 하나 생성해보았고, mocking된 userSerive를 UserController 객체를 하나 생성하여 주입해주고 상속받은 initController 메서드를 구성해준다. 그렇면 mocking된 UserService를 사용해 중복 검증을 줄이고 docs 생성을 최우선으로 만들 수 있다.
docs 생성을 위해 테스트 코드를 작성할텐데, 전에 만들어둔 Controller 테스트를 하나 긁어와보겠다.
@DisplayName("이메일 검증 API")
@Test
void checkMatchEmail() throws Exception {
// when // then
mockMvc.perform(
// MockMvcRequestBuilders.get("/auth/check")
RestDocumentationRequestBuilders.get("/auth/check")
.param("email", "test@test.com")
)
.andDo(print())
.andExpect(status().isOk());
}
⚠️ MockMvcRequestBuilders 보다 RestDocumentationRequestBuilders를 사용하는 것을 권장한다
해당 코드는 이메일을 검증하는 API이고 해당 주소로 호출이 잘되는지, 값에 대한 체크를 해볼 수 있다. 이 코드는 이미 검증된 코드인데, 이제 이 코드를 가지고 그대로 docs 문서로 만들어 볼 것 이다.
@DisplayName("이메일 검증 API")
@Test
void checkMatchEmail() throws Exception {
// given
given(userService.emailCheckMatch(anyString()))
.willReturn(true);
// when // then
mockMvc.perform(
MockMvcRequestBuilders.get("/auth/check")
.param("email", "1")
)
.andDo(print())
.andExpect(status().isOk())
.andDo(document("user-emailCheck",
preprocessResponse(prettyPrint()),
requestParameters(
parameterWithName("email")
.description("회원가입 하려는 이메일")
),
responseFields(
fieldWithPath("code").type(JsonFieldType.NUMBER)
.description("상태 코드"),
fieldWithPath("message").type(JsonFieldType.STRING)
.description("응답 메시지"),
fieldWithPath("data").type(JsonFieldType.BOOLEAN)
.description("이메일 존재 여부")
)
));
}
... docs를 적용하면 response가 별게없어도 코드가 길어진다. 위에서 말한대로 테스트 코드와 docs 테스트 코드를 분리하라는 이유가 이거다. 여기서 response가 page나 list라고 생각하면 실제로 하나의 테스트 코드가 100줄이 넘어간다(...)
아무튼 코드에 대한 설명을 해보자면
RestDocs는 테스트의 수행, 결과에 따라 문서가 만들어진다. 즉, mocking된 Service를 Stubbing하여 문서에 Response 될 값을 만들어 준다.
해당 코드 내부에서 docs를 구성한다. 하나씩 살펴보면 첫번째 파라미터 값으로 파일이 생성되는 이름을 만들어 주고나서 메서드로 docs를 작성한다.
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
💡 Request / Response 를 지정할때 테스트의 Request / Response 와 동일하게 맞춰주어야 한다 !
requestParameters(
parameterWithName("name")
.description("유저 이름")
),
requestHeaders(
headerWithName("Authorization")
.description("insert the AccessToken")
),
requestFields(
fieldWithPath("email").type(JsonFieldType.STRING)
.description("유저 이메일"),
fieldWithPath("password").type(JsonFieldType.STRING)
.description("유저 비밀번호")
),
requestParts(
partWithName("file")
.description("변경할 이미지")
),
pathParameters(
parameterWithName("contentId")
.description("피드 id")
),
⚠️ pathParameters()를 이용하게 되는 경우 MockMvcRequestBuilders()로 요청하는 것이 아닌 RestDocumentationRequestBuilders()로 요청해야한다.
mockMvc.perform(RestDocumentationRequestBuilders.get("/file/{fileId}", 1L))
responseFields(
fieldWithPath("code").type(JsonFieldType.NUMBER)
.description("상태 코드"),
fieldWithPath("message").type(JsonFieldType.STRING)
.description("상태 메세지"),
fieldWithPath("data.id").type(JsonFieldType.NUMBER)
.description("유저 ID / Long")
)
테스트 작성이 완료되었다면, 테스트를 실행해보고 정상적으로 통과하는지 살펴보자. 만약 정상적으로 통과했다면 docs를 생성해준다.
gradle 탭에서 위와 같은 패키지 구조로 이동해서 asciidoctor를 실행해준다. 처음에는 생각보다 오래걸리니 인내심을 갖고 기다린 후 해당 테스트가 완료되면
프로젝트의 패키지에서 build -> genreated-snippetes에 위와같이 adoc 문서가 생성된 것을 알 수 있다. 하지만 우리는, 하나의 페이지에 담아서 API 문서를 만들고 배포를 하려하는것이지 위와같이 조각 나있는 것을 원하지 않기에 이제 해당 문서를 하나로 합칠 것 이다.
src 패키지 하위에 바로 docs 라는 패키지를 만들고 docs 하위에 asciidoc 패키지를 만든 후 index.adoc 파일을 생성해주자, 만약 프로젝트에 API가 많아졌다면 쉽게 구분할 수 있게끔 api 패키지를 만들어 관리하자. 참고로 여기서 꼭!! 패키지 구분과 파일의 위치를 위와 같이 해야한다.
이유는 build.gradle에 우리가 적었듯이, 해당 패키지를 인식하고 index.html을 자동으로 생성해주기 때문.
API 패키지를 만든 후 userAPI를 담을 파일을 하나 더 만들어서 아까 자동으로 생성된 조각나있는 adoc 파일을 하나로 모아준다. 사실 모든 조각을 합칠필요는 없고 꼭 필요한것만 문서에 담도록 하자.
UserAPI를 모은 문서를 아까 생선한 index.adoc에 다시 담아주자. 이렇게 하면 API가 많아져도 분리해서 관리할 수 있기 때문에 가독성과 효율성을 챙길 수 있다.
이제 이 상태에서 gradle 탭에 asciidoctor 테스트 를 시 돌려주면...
build 패키지에 자동으로 docs패키지와 index.html이 생성된 것을 확인할 수 있다. 그리고 해당 html 파일을 인터넷 브라우저로 열어보면..
docs는 초반 세팅이 확실히 어렵다. 그럼에도 불구하고 테스트 코드와 연관되어 API 문서의 정확성을 향상시킬 수 있다는 점이 강점이다. 또한 swagger 처럼 프로덕션 코드에 간섭이 되어있지 않는다는 장점도 있기에 초반 세팅만 잘해서 잘 사용하도록 하자 !
한번 만들어놓으면 문서 만드는 재미도 있다 🤣