반응형
1. Spring REST Docs 사용하기 - 설정
2. Spring REST Docs 사용하기 - 테스트코드 작성과 adoc
3. Spring REST Docs 사용하기 - 커스텀

Spring REST Docs의 문서는 앞서 이야기했듯이, Spring Framework의 MockMVC, Webflux의 WebClient를 사용해서 만들어진 테스트에서 생성된 스니펫을 사용하여 문서를 생성한다. 그렇기 때문에 컨트롤러 테스트코드의 작성은 필수다.

 

간단하게 하나의 컨트롤러 테스트를 만들어 문서가 생성되는것을 확인해보자

Controller Test 만들기

Spring WebMVC를 사용했기에 MockMVC를 사용해서 컨트롤러 테스트를 만들어보자.

@WebMvcTest(
    controllers = [OrderController::class],
    excludeAutoConfiguration = [SecurityAutoConfiguration::class],
)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@AutoConfigureRestDocs  // 1
class OrderControllerTest(
    private val mockMvc : MockMvc,
    private val mapper : ObjectMapper,
) {

    @Test
    fun defaultGet() {
        this.mockMvc.perform(
            MockMvcRequestBuilders.get("/orders/{id}", 1)
        ).andExpect(MockMvcResultMatchers.status().isOk)
    }

    @Test
    fun defaultPost() {
        val request = mapper.writeValueAsString(OrderRequest("userId", items = listOf(OrderItem(1L, "상품1", 3))))
        this.mockMvc.perform(
            MockMvcRequestBuilders.post("/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(request)
        ).andExpect(MockMvcResultMatchers.status().isCreated)
    }

}

기본적인 컨트롤러 테스트코드만 작성했다. 컨트롤러가 어떻게 구현되어있는지보다 테스트코드를 통해 어떻게 문서가 생성되는지 확인하는데 집중해보자. 물론 테스트코드는 문제없이 정상적으로 돌아가게 만들어야한다. 그리고 @AutoConfigureRestDocs을 사용하여 RestDocs에 대한 자동설정을 사용하자.

문서를 작성하는데 컨트롤러 테스트만 진행할것이기 때문에 @WebMvcTest를 통하여 컨트롤러들만 컨텍스트에 빈으로 올라오게 하던가, @WebMvcTest의 속성중 controllers를 사용하여 특정 컨트롤러만 컨텍스트에 올라오게 하자. 
여담으로 위의코드는 Spring Security가 적용되어있어서 우회하기 위해 @WebMvcTest 속성중 excludeAutoConfiguration에 SecurityAutoConfiguration을 등록하여 제외하게 했다.

Adoc문서 생성

각 테스트코드의 마지막 부분에 다음과 같이 MockMvcRestDocumentation.document을 사용하고 요청필드, 응답필드를 설정한다

@Test
fun defaultGet() {
    this.mockMvc.perform(
        // 1
        RestDocumentationRequestBuilders.get("/orders/{id}", 1)
    ).andDo(MockMvcResultHandlers.print())
        .andExpect(MockMvcResultMatchers.status().isOk)
        .andDo(MockMvcRestDocumentation.document(
            // 2
            "order/defaultGet",  
            // 3
            RequestDocumentation.pathParameters(
                RequestDocumentation.parameterWithName("id").description("Order ID")
            ),
            // 4
            PayloadDocumentation.responseFields(
                PayloadDocumentation.fieldWithPath("id").description("주문ID"),
                PayloadDocumentation.fieldWithPath("name").description("받는사람"),
            )
        )
    )
}

@Test
fun defaultPost() {
    val request = mapper.writeValueAsString(OrderRequest("userId", items = listOf(OrderItem(1L, "상품1", 3))))
    this.mockMvc.perform(
        MockMvcRequestBuilders.post("/orders")
            .contentType(MediaType.APPLICATION_JSON)
            .content(request)
    ).andExpect(MockMvcResultMatchers.status().isCreated)
        .andDo(
        MockMvcRestDocumentation.document(
            // 2
            "{ClassName}/{MethodName}",
            // 3
            PayloadDocumentation.requestFields(
                PayloadDocumentation.fieldWithPath("userId").description("유저ID"),
                PayloadDocumentation.fieldWithPath("items").description("상품목록"),
                PayloadDocumentation.fieldWithPath("items[].id").description("상품ID"),
                PayloadDocumentation.fieldWithPath("items[].name").description("상품명"),
                PayloadDocumentation.fieldWithPath("items[].quantity").description("수량"),
            ),
            // 4
            PayloadDocumentation.responseFields(
                PayloadDocumentation.fieldWithPath("userId").description("유저아이디"),
                PayloadDocumentation.fieldWithPath("items").description("description"),
                PayloadDocumentation.fieldWithPath("items[].id").description("상품ID"),
                PayloadDocumentation.fieldWithPath("items[].name").description("상품명"),
                PayloadDocumentation.fieldWithPath("items[].quantity").description("수량")
            )
        )
    )
}

1. RestDocs을 사용할때 Get 메소드에 대한 문서를 작성하기 위해서 MockMvcRequestBuilders에서 RestDocumentationRequestBuilders의 get으로 변경했다.

2. 문서를 생성할 위치를 지정한다. 문자열로 직접 지정할 수 있고, RestDocs에서 제공하는 Output Parameter를 사용하면 ClassName/MehtodName 같이 만들수 도 있다. 자세한 내용은 아래의 링크를 참고하자

 

https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#documentating-your-api-parameterized-output-directories

 

Spring REST Docs

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

docs.spring.io

3. 테스트코드에서 사용하는 PathParameter 또는 RequestField 항목에 대한 정의를 작성한다

4. 응답필드로 어떤 타입의 필드가 반환되는지 정의한다. 반환되는 json 응답에 대해 필드의 정의가 빠져있다면 에러가 나올것이다.

 

이렇게 코드를 작성했다면 테스트코드를 실행해보자. 테스트가 성공적으로 끝났다면 Rest Docs의 문서 생성 output 디렉토리(기본은 Gradle 기준으로 build/generated-snippets)에 문서가 생성되어있을것이다.

이렇게 테스트코드(정확하게는 컨트롤러 테스트)를 작성하여 나오는 결과에 대해 adoc 문서를 작성할 수 있게 되었다.이 문서들을 사용해서 외부에 공개할 수 있는 HTML문서로 변경하는 작업을 해보자

HTML 문서로 변환하기

만들어진 스니펫(adoc)을 HTML로 변환하기 위해서는 Gradle 기준으로 아래에 위치에 디렉토리를 생성하고 index.adoc 파일을 생성한다. 파일이름은 index가 아닌 다른 이름으로 만들어도 되며, 이 이름으로 HTML파일이 생성된다.

 

src/docs/asciidoc/*.adoc

메이븐은 다음 링크를 참조하면 된다

https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#getting-started-using-the-snippets

 

Spring REST Docs

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

docs.spring.io

 

만약에 위의 경로에 adoc파일이 여러개 있다면, 모든 adoc 파일을 HTML로 변환해버리기 때문에 하나의 adoc만 변환하기를 원한다면 다음과 같이 build.gradle 설정에서 sources 설정을 해주면된다. 

tasks {
	...
    asciidoctor {
		...
        sources {
            include("**/index.adoc")
        }
        
        baseDirFollowsSourceFile()
		...
    }
}

생성한 파일을 열고 아래와 같이 작성해보자

= REST API

[[Order]]

== 주문 API

include::{snippets}/OrderControllerTest/create/http-request.adoc[]
IntelliJ IDEA를 사용한다면 AsciiDoc 플러그인을 설치하자. 미리보기를 제공하여 한결 adoc 문서를 작성하기 쉬워진다

include를 사용하여 원하는 adoc을 삽입할 수 있다. {snippets}는 생성된 스니펫이 있는 폴더(Spring REST 문서만 해당)가 지정되어있다. 나중에 컨버팅 할때 스니펫이 있는 폴더로 치환된다.

 

이제, Gradle의 build 또는 asciidoctor Task를 실행해보면 아래와 같이 HTML 문서가 생성된것을 볼 수 있다.

 

이제 index.html 문서를 볼 수 있게 해야한다. build.gralde.kts에서 다음과 같이 추가한다

task {
    ...생략
    bootJar {
        dependsOn(asciidoctor)
        from("build/docs/asciidoc") {
            into("BOOT-INF/classes/static/docs")
        }
    }

    build {
        dependsOn("copyFile")
    }

    register<Copy>("copyFile") {
        dependsOn(asciidoctor)

        destinationDir = file("src/main/resources/static")

        delete("src/main/resources/static/docs")
        from("build/docs/asciidoc") {
            this.into("docs")
        }
    }
}

bootJar Task에서 설정한 내용은 Jar파일로 만들어 질때 BOOT-INF 하위로 index.html을 복사한다. 이렇게 되면 Jar를 통해 실행할 때 index.html을 볼 수 있다.

build Task에서 설정한 내용은 IntelliJ IDEA에서 Run할 때 볼수 있기 위함이다. 실제로 src/main/resources/static/docs 하위에 index.html이 있는것을 볼 수 있다

 

서버를 실행시키고 localhost:포트/docs/index.html을 브라우저에서 보면 다음과같이 나타나게 된다. 아래는 index.html을 직접 연 화면이다

예시를 위해 작성중인 API문서로 대체했다

위의 그림과 같이 왼쪽에 toc를 나타나게 하려면 아래와 같이 index.adoc을 수정한다

= REST API
:toc: left
:toc-title: 목차
:toclevels: 2
:source-highlighter: highlightjs

[[Order]]

== 주문 API

include::{snippets}/OrderControllerTest/create/http-request.adoc[]

 

다음 글에서 Rest Docs을 좀더 커스터마이징(e.g 스니펫)하여 사용할 수 있는 내용을 작성하도록 할까 한다

반응형

+ Recent posts