WEB/Spring RestDoc

Spring REST Docs

Tony Lim 2023. 4. 18. 13:35

Spring Rest Docs

장점

  • 운영중인 코드에 영향을 미치지 않음
  • 변경된 기능에 대해서 최신문서 유지가 어느정도 가능함
  • Test케이스 실행 -> 문서를 자동으로 생성해줌
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.7'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id "org.asciidoctor.jvm.convert" version "3.3.2"
}

group = 'com.tony'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    asciidoctorExt
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

ext {
    asciidocVersion = "2.0.6.RELEASE"
    snippetsDir = file('build/generated-snippets')
}


dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    implementation 'com.querydsl:querydsl-core'
    implementation 'com.querydsl:querydsl-jpa'

    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    asciidoctorExt "org.springframework.restdocs:spring-restdocs-asciidoctor:${asciidocVersion}"
    testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:${asciidocVersion}"
}

tasks.named('test') {
    useJUnitPlatform()
}

test {
    outputs.dir snippetsDir
}

asciidoctor {
    inputs.dir snippetsDir
    configurations 'asciidoctorExt'
    dependsOn test
}

bootJar {
    dependsOn asciidoctor

    copy {
        from asciidoctor.outputDir
        into "src/main/resources/static/docs"
    }
}

asciidoctor task를 통해서 만들어진 adoc 파일을 statc/docs 로 옮겨서 localhost:8080/docs/index.html 으로 get요청을 날렸을시 

이런식으로 조회가 가능하다.

@SpringBootTest
@ExtendWith(RestDocumentationExtension.class)
public class PostControllerDocTest {

    private MockMvc mockMvc;

    @Autowired
    private PostRepository postRepository;

    @BeforeEach
    void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(documentationConfiguration(restDocumentation))
                .build();
    }

    @Test
    @DisplayName("글 단건 조회 테스트")
    void test1() throws Exception {
        // given
        Post post = Post.builder()
                .title("123456789012345")
                .content("bar")
                .build();
        postRepository.save(post);
        // expected
        mockMvc.perform(MockMvcRequestBuilders.get("/posts/{postId}",1).accept(MediaType.APPLICATION_JSON))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcRestDocumentation.document("index"));
    }
}

통과한 test를 기반으로 rest docs api 문서를 만들게된다.

build/generated-snippets/index 에 여러 adoc파일들이 생기게 된다. 

= 블로그 API
:toc:

== 글 단건 조회

=== 요청
include::{snippets}/index/http-request.adoc[]

=== 응답
include::{snippets}/index/http-response.adoc[]

=== CURL
include::{snippets}/index/curl-request.adoc[]

만들어진 adoc 파일들을 기반으로 src/docs/asciidoc/index.adoc 파일에 만들어진것들을 import하고 꾸미민후에 
bootjar task를 통해서 static 폴더 밑으로 index.html이 생성되게 된다.


@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs(uriScheme = "https", uriHost = "api.blog.com",uriPort = 443)
@ExtendWith(RestDocumentationExtension.class)
public class PostControllerDocTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private PostRepository postRepository;

    @BeforeEach
    void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(documentationConfiguration(restDocumentation))
                .build();
    }

localhost, 같은 주소로 나오지 않게 @AutoConfigureRestDocs를 통해 원하는 포멧을 지정하였다.

기존에는 MockMvc를 setup에서 만들어서 주입하였지만 @AutoConfigureMockMvc를 통해 스프링에서 @AutoConfigureRestDocs가 반영된 MockMvc를 주입할 수 있도록 변경하였다.

변경이 된것을 확인

응답필드에 대한 설명이 부족하다. Path parameter 인 postId에 대한 설명과 repsonse필드에 대한 설명을 추가해보자

@Test
@DisplayName("글 단건 조회 테스트")
void test1() throws Exception {
    // given
    Post post = Post.builder()
            .title("123456789012345")
            .content("bar")
            .build();
    postRepository.save(post);
    // expected
    mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/{postId}",1).accept(MediaType.APPLICATION_JSON))
            .andDo(MockMvcResultHandlers.print())
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andDo(MockMvcRestDocumentation.document("index",
                    RequestDocumentation.pathParameters(
                        RequestDocumentation.parameterWithName("postId").description("게시글 ID")
                    ),
                    PayloadDocumentation.responseFields(
                        PayloadDocumentation.fieldWithPath("id").description("게시글 ID"),
                        PayloadDocumentation.fieldWithPath("title").description("제목"),
                        PayloadDocumentation.fieldWithPath("content").description("내용")
                    )
            ));
}

 RestDocumentationRequestBuilder의 get을 써야 RequestDocumentation의 메소드들을 정상적으로 사용할 수 있다. MockMvcRequestBuilders 랑 동일한 클래스를 리턴하기에 문제없이 쓸 수 있다.

중간에 "content" 에 대한 description을 빼먹는다면 해당 테스트는 repsonse field 중에 id,title 은 설명이 추가되어있는데 content에 대한 설명은 빠져있다고 오류를 내뱉는다.

= 블로그 API
:toc:

== 글 단건 조회

=== 요청
include::{snippets}/index/http-request.adoc[]

=== 요청 파라메터
include::{snippets}/index/path-parameters.adoc[]

=== 응답
include::{snippets}/index/http-response.adoc[]

include::{snippets}/index/response-fields.adoc[]

=== CURL
include::{snippets}/index/curl-request.adoc[]

추가로 생성된 adoc을 index.adoc에 추가해주면된다.

 


https://docs.spring.io/spring-restdocs/docs/2.0.6.RELEASE/reference/html5/#documenting-your-api-customizing-including-extra-information

 

Spring REST Docs

Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test, WebTestClient, or REST Assured.

docs.spring.io

에 나온것처럼 2가지 방법으로 customzing할 수 있다.

@DisplayName("글 등록")
void test2() throws Exception {
    // given
    PostCreate request = PostCreate.builder()
            .title("호돌맨")
            .content("반포자이")
            .build();

    String json = objectMapper.writeValueAsString(request);

    // expected
    mockMvc.perform(post("/posts")
                    .contentType(APPLICATION_JSON)
                    .accept(APPLICATION_JSON)
                    .content(json))
            .andDo(print())
            .andExpect(status().isOk())
            .andDo(document("post-create",
                    requestFields(
                            fieldWithPath("title").description("제목")
                                    .attributes(key("constraint").value("좋은제목 입력해주세요.")),
                            fieldWithPath("content").description("내용").optional()
                    )
            ));
}

attribute() , optional() 같은 메소드를 추가하는 방법과

|===
|Path|Type|Description|Optional|Constraint

{{#fields}}
|{{path}}
|{{type}}
|{{description}}
|{{#optional}}Y{{/optional}}
|{{#constraint}} {{.}} {{/constraint}}
{{/fields}}
|===

org/springframework/restdocs/templates/asciidoctor/request-fields.snippet 에 orverride할 snippet을 적어주면 원하는대로 덮어 씌워줄 수 있다.