Swagger
와 Spring Rest Docs
가 있다.Rest Docs
가 문서 작성에 필요한 코드 조각을 만드는 도구라면 Asciidoctor
는 Adoc 파일을 활용하여 html 문서를 만들어주는 도구이다.build.gradle
에 아래와 같이 필요한 의존성과 플러그인 등을 추가해주자.// build.gradle
plugins {
...
id "org.asciidoctor.convert" version "1.5.9.2" // 추가
...
}
dependencies {
...
asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.4.RELEASE' // 추가
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.4.RELEASE' // 추가
...
}
// 밑에 전부 추가
ext {
snippetsDir = file('build/generated-snippets')
}
test {
useJUnitPlatform()
outputs.dir snippetsDir
}
asciidoctor {
inputs.dir snippetsDir
dependsOn test
}
bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
@WebMvcTest
어노테이션을 사용하면 Spring MVC와 관련된 빈들만 애플리케이션 컨텍스트에 로드하여 보다 가벼운 slice 테스트가 가능하다.@WebMvcTest(PostController.class)
class PostControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private PostService postService;
@Test
public void create() throws Exception {
final PostResponse post = PostResponse.builder()
.id(1L)
.title("first_post")
.content("hello_my_world")
.build();
given(postService.createPost(any())).willReturn(post);
mvc.perform(post("/posts")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"first_post\",\"content\":\"hello_my_world\"}"))
.andExpect(status().isCreated())
.andExpect(header().stringValues("location", "/posts/" + 1L))
.andDo(print());
}
@Test
public void readAll() throws Exception {
final List<PostResponse> posts = new ArrayList<>();
posts.add(PostResponse.builder().id(1L).title("one").content("111").build());
posts.add(PostResponse.builder().id(2L).title("two").content("222").build());
given(postService.getPosts()).willReturn(posts);
mvc.perform(get("/posts"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"id\":1,\"title\":\"one\",\"content\":\"111\"")))
.andExpect(content().string(containsString("\"id\":2,\"title\":\"two\",\"content\":\"222\"")))
.andDo(print());
}
@Test
public void read() throws Exception {
final Post post = Post.builder()
.id(63L)
.title("ys")
.content("is bossdog")
.build();
given(postService.getPostById(any())).willReturn(post);
mvc.perform(get("/posts/" + post.getId()))
.andExpect(status().isOk())
.andExpect(content().string(
containsString("\"id\":63,\"title\":\"ys\",\"content\":\"is bossdog\"")))
.andDo(print());
}
@Test
public void update() throws Exception {
final Post post = Post.builder()
.id(5L)
.title("before_title")
.content("before_content")
.build();
given(postService.getPostById(any())).willReturn(post);
mvc.perform(put("/posts/" + post.getId())
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"changed_title\",\"content\":\"changed_content\"}"))
.andExpect(status().isOk())
.andDo(print());
verify(postService).updatePost(eq(5L), any());
}
@Test
public void destroy() throws Exception {
final Post post = Post.builder()
.id(100L)
.title("title")
.content("content")
.build();
mvc.perform(delete("/posts/" + post.getId()))
.andExpect(status().isNoContent())
.andDo(print());
verify(postService).destroyPost(eq(100L));
}
}
public class PostDocumentation {
public static RestDocumentationResultHandler createPost() {
return document("posts/create",
requestFields(
fieldWithPath("title").type(JsonFieldType.STRING).description("This is post title."),
fieldWithPath("content").type(JsonFieldType.STRING).description("This is post content")
)
);
}
public static RestDocumentationResultHandler getPosts() {
return document("posts/getAll",
responseFields(
fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("This is post id"),
fieldWithPath("[].title").type(JsonFieldType.STRING).description("This is post title"),
fieldWithPath("[].content").type(JsonFieldType.STRING).description("This is post content")
)
);
}
public static RestDocumentationResultHandler getPost() {
return document("posts/get",
pathParameters(
parameterWithName("id").description("This is post id")
),
responseFields(
fieldWithPath("id").type(JsonFieldType.NUMBER).description("This is post id"),
fieldWithPath("title").type(JsonFieldType.STRING).description("This is post title"),
fieldWithPath("content").type(JsonFieldType.STRING).description("This is post content")
)
);
}
public static RestDocumentationResultHandler updatePost() {
return document("posts/update",
pathParameters(
parameterWithName("id").description("This is post id")
),
requestFields(
fieldWithPath("title").type(JsonFieldType.STRING).description("This is post title."),
fieldWithPath("content").type(JsonFieldType.STRING).description("This is post content")
)
);
}
public static RestDocumentationResultHandler deletePost() {
return document("posts/delete",
pathParameters(
parameterWithName("id").description("This is post id")
)
);
}
}
@ExtendWith(RestDocumentationExtension.class)
@WebMvcTest(PostController.class)
class PostControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private PostService postService;
@BeforeEach
public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.addFilter(new ShallowEtagHeaderFilter())
.apply(documentationConfiguration(restDocumentation))
.build();
}
...
// 각 테스트 마지막에 다음과 같이 .andDo(PostDocumentation.xx)를 호출해준다.
@Test
public void create() throws Exception {
...
mvc.perform(post("/posts")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"first_post\",\"content\":\"hello_my_world\"}"))
.andExpect(status().isCreated())
.andExpect(header().stringValues("location", "/posts/" + 1L))
.andDo(print())
.andDo(PostDocumentation.createPost()); // 추가
}
// 조회와 같이 특정 요소의 id값을 path로 전달해야하는 경우는 RestDocumentationRequestBuilder를 사용한다.
@Test
public void read() throws Exception {
...
mvc.perform(RestDocumentationRequestBuilders.get("/posts/{id}", post.getId()))
.andExpect(status().isOk())
.andExpect(content().string(
containsString("\"id\":63,\"title\":\"ys\",\"content\":\"is bossdog\"")))
.andDo(print())
.andDo(PostDocumentation.getPost());
}
documentation
>asccidoc
>api-guide.adoc
파일을 추가한다.ifndef::snippets[]
:snippets: ../../../build/generated-snippets
endif::[]
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
:operation-http-request-title: Example Request
:operation-http-response-title: Example Response
[[resources]]
= Resources
[[resources-posts]]
== Post
[[resources-posts-create]]
=== 포스트 추가
operation::posts/create[snippets='http-request,http-response,request-fields,request-body']
[[resources-posts-getAll]]
=== 포스트 전체 조회
operation::posts/getAll[snippets='http-request,http-response,response-body']
[[resources-posts-get]]
=== 포스트 조회
operation::posts/get[snippets='http-request,http-response,response-body']
[[resources-posts-update]]
=== 포스트 수정
operation::posts/update[snippets='http-request,http-response,request-fields,request-body']
[[resources-posts-delete]]
=== 포스트 삭제
operation::posts/delete[snippets='http-request,http-response']
Tasks
>documentation
>asciidoctor
를 실행시키면 html문서를 만들 수 있다. (intelliJ 플러그인을 설치하면 IDE안에서 API문서를 볼 수도 있다.)
Spring REST Docs는 Test를 기반으로 API 문서를 자동으로 작성해주는 도구이다.
간단한 CRUD 예제를 통해 API 문서 작성을 자동화해보았다.
Production 코드에 추가적인 코드 작성을 하지 않아도 되는 장점이 있지만, 각각의 API마다 템플릿을 지정해야하는 번거로움이 따른다.
그래서 어노테이션 기반의 swagger를 선호하는 사람들도 많다. 다음에는 swagger를 사용한 API 문서 작성 방법에 대해 공부해봐야겠다.
Spring Rest Docs 적용 - 우형 기술 블로그
Spring REST Docs에 날개를... (feat: Popup) - 우형 기술 블로그