[Spring REST Docs] 적용 과정 및 가이드 - 1

Coastby·2023년 1월 29일
0

Daengnyang 프로젝트

목록 보기
7/12
post-custom-banner

✏️ 적용 과정

1. build.gradle 설정

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.1'
    id 'io.spring.dependency-management' version '1.1.0'
    // Asciidoctor 플러그인 적용
    // gradle 7.0 이상부터는 jvm 사용
    id "org.asciidoctor.jvm.convert" version "3.3.2"
}

group = 'com.daengnyangffojjak'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    asciidoctorExtensions // dependencies 에서 적용한 것 추가
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.24'
    annotationProcessor 'org.projectlombok:lombok:1.18.24'
    ...
    implementation 'org.springframework.security:spring-security-test:6.0.1'    //버전확인
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // (1)
    asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    // (2)
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    testCompileOnly 'org.projectlombok:lombok:1.18.24' // 테스트 의존성 추가
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.24' // 테스트 의존성 추가
}

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

ext {
    // 아래서 사용할 변수 선언
    snippetsDir = file('build/generated-snippets')
}


test {
    // 위에서 작성한 snippetsDir 디렉토리를 test의 output으로 구성하는 설정 -> 스니펫 조각들이 build/generated-snippets로 출력
    outputs.dir snippetsDir
    useJUnitPlatform()
}

// asciidoctor 설정
asciidoctor { 
    dependsOn test // test 작업 이후에 작동하도록 하는 설정
    configurations 'asciidoctorExtensions' // 위에서 작성한 configuration 적용
    inputs.dir snippetsDir // snippetsDir 를 입력으로 구성

    // source가 없으면 .adoc파일을 전부 html로 만들어버림
    // source 지정시 특정 adoc만 HTML로 만든다.
    sources{
        include("**/index.adoc","**/common/*.adoc")
    }

    // 특정 .adoc에 다른 adoc 파일을 가져와서(include) 사용하고 싶을 경우 경로를 baseDir로 맞춰주는 설정입니다.
    // 개별 adoc으로 운영한다면 필요 없는 옵션입니다.
    baseDirFollowsSourceFile()
}

// asciidoctor가 실행될 때 static/docs 폴더 비우기
asciidoctor.doFirst {
    delete file('src/main/resources/static/docs')
}

// asccidoctor 작업 이후 생성된 HTML 파일을 static/docs 로 copy
task copyDocument(type: Copy) {
    dependsOn asciidoctor
    from file("build/docs/asciidoc")
    into file("src/main/resources/static/docs")
}

// build 시 codyDocument 실행
build {
    dependsOn copyDocument
}

//build의 bootJar 시 asciidoctor에 의존하여 /static/docs에 index.html이 생성된다. 
bootJar {
    dependsOn asciidoctor
    from("${asciidoctor.outputDir}") {
        into 'static/docs'
    }
}

(1) spring-restdocs-asciidoctor
build/generated-snippets 에 생긴 .adoc 조각들을 프로젝트 내의 .adoc 파일에서 읽어들일 수 있도록 연동해준다.
이 덕분에 .adoc 파일에서 operation 같은 매크로를 사용하여 스니펫 조각들을 연동할 수 있다.
그리고 최종적으로 .adoc 파일을 HTML로 만들어 export 해준다.

(2) spring-restdocs-mockmvc
restdocs-mockmvc의 testCompile 구성 -> mockMvc를 사용해서 snippets 조각들을 뽑아낼 수 있게 된다.
MockMvc 대신 WebTestClient을 사용하려면 spring-restdocs-webtestclient 추가.
MockMvc 대신 REST Assured를 사용하려면 spring-restdocs-restassured 를 추가.

2.  (회원가입 )로직 구현

    @PostMapping(value = "/join")       //회원가입
    public ResponseEntity<Response<UserJoinResponse>> join(@RequestBody UserJoinRequest request){
        UserJoinResponse userJoinResponse = userService.join(request);
        return ResponseEntity.created(URI.create("/api/v1/users/"+userJoinResponse.getId()))     //성공 시 상태코드 : 201
                .body(Response.success(userJoinResponse));
    }

3. ⌨️ RestDocsConfiguration 작성

.acod 파일 형식을 설정하고, 테스트 작성을 편리하게 하기 위해서 몇가지 설정을 추가한다.

(1) .adoc 파일이 저장될 폴더명을 클래스이름-메소드명으로 자동으로 지정해준다.

(2), (3) response, request body의 JSON이 보기 좋게 이쁘게 출력된다.

(4) 커스텀으로 넣는 컬럼을 넣기 편하게 사용할 메서드이다.

@TestConfiguration
public class RestDocsConfiguration {

    @Bean
    public RestDocumentationResultHandler write(){
        return MockMvcRestDocumentation.document(
                "{class-name}/{method-name}",		// (1) 
                Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),	// (2)
                Preprocessors.preprocessResponse(Preprocessors.prettyPrint())	// (3)
        );
    }

    public static final Attribute field(	// (4)
            final String key,
            final String value){
        return new Attribute(key,value);
    }
}

4. 테스트 코드 작성

@WebMvcTest(UserRestController.class)
@AutoConfigureRestDocs
@WithMockUser
@Import(RestDocsConfiguration.class)
@ExtendWith(RestDocumentationExtension.class)
class UserRestControllerTest {
    @MockBean
    UserService userService;
    @Autowired
    ObjectMapper objectMapper;
    @Autowired
    protected RestDocumentationResultHandler restDocs;
    @Autowired
    private MockMvc mockMvc;
    UserJoinRequest userJoinRequest = new UserJoinRequest("hoon", "hi", "gg@gmail.com");


    @BeforeEach
    void setUp(final WebApplicationContext context,
               final RestDocumentationContextProvider provider) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(MockMvcRestDocumentation.documentationConfiguration(provider))  // rest docs 설정 주입
                .alwaysDo(MockMvcResultHandlers.print()) // andDo(print()) 코드 포함
                .alwaysDo(restDocs) // pretty 패턴과 문서 디렉토리 명 정해준것 적용
                .addFilters(new CharacterEncodingFilter("UTF-8", true)) // 한글 깨짐 방지
                .build();
    }

    @Nested
    @DisplayName("회원가입")
    class join{
        @Test
        @DisplayName("회원가입 성공")
        void join_success() throws Exception {
            given(userService.join(userJoinRequest)).willReturn(new UserJoinResponse(0L, "hoon", "gg@gmail.com"));

            mockMvc.perform(
                            post("/api/v1/users/join")
                                    .content(objectMapper.writeValueAsBytes(userJoinRequest))
                                    .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isCreated())
                    .andExpect(jsonPath("$.result.userName").value("hoon"))
                    .andExpect(jsonPath("$.result.id").value(0))
                    .andDo(
                            restDocs.document(
                                    requestFields(
                                            fieldWithPath("userName").description("유저아이디").attributes(field("constraints", "중복 불가능")),
                                            fieldWithPath("password").description("비밀번호"),
                                            fieldWithPath("email").description("이메일").attributes(field("constraints", "중복 불가능"))
                                    ),
                                    responseFields(
                                            fieldWithPath("resultCode").description("결과코드"),
                                            fieldWithPath("result.id").description("유저번호"),
                                            fieldWithPath("result.userName").description("유저아이디"),
                                            fieldWithPath("result.email").description("이메일"))
                            )
                    );
            verify(userService).join(userJoinRequest);
        }
    }
}

@AutoConfigureRestDocs

  • Rest docs 자동 설정

@Import(RestDocsConfiguration.class)

  • 위에서 작성한 RestDocsConfiguration을 읽어 올 수 있게 한다.

@ExtendWith(RestDocumentationExtension.class)

  • setUp에서 주입받을 RestDocumentationContextProvider를 읽어올 수 있게 한다.

📌 실제 작성 시 신경 쓸 부분

.andDo(
        restDocs.document(
                requestFields(
                        fieldWithPath("userName").description("유저아이디").attributes(field("constraints", "중복 불가능")),
                        fieldWithPath("password").description("비밀번호"),
                        fieldWithPath("email").description("이메일").attributes(field("constraints", "중복 불가능"))
                ),
                responseFields(
                        fieldWithPath("resultCode").description("결과코드"),
                        fieldWithPath("result.id").description("유저번호"),
                        fieldWithPath("result.userName").description("유저아이디"),
                        fieldWithPath("result.email").description("이메일"))
        )
);

예시 1 - get

.andDo(
        restDocs.document(
                pathParameters(
                        parameterWithName("id").description("Member ID")
                ),
                responseFields(
                        fieldWithPath("id").description("ID"),
                        fieldWithPath("age").description("age"),
                        fieldWithPath("email").description("email")
                )
        )
)

예시 2 - get

.andDo(
        restDocs.document(
                requestParameters(
                        parameterWithName("size").optional().description("size"),
                        parameterWithName("page").optional().description("page")
                )
        )
)

예시 3 - modify

.andDo(
        restDocs.document(
                pathParameters(
                        parameterWithName("id").description("Member ID")
                ),
                requestFields(
                        fieldWithPath("age").description("age")
                        fieldWithPath("email").optional().description("email")

                )
        )
)

5. .adoc 파일 확인

테스트 작성 후 테스트를 통과하게 되면 아래 경로에 adoc 조각 파일들이 생성된다.

기본적으로 다음과 같은 조각들이 default로 생성된다.

  • curl-request.adoc
  • http-request.adoc
  • httpie-request.adoc
  • http-response.adoc
  • request body
  • response body

테스트 코드에 따라 추가적인 조각이 생성될 수 있다.

  • response-fields.adoc
  • request-parameters.adoc
  • request-parts.adoc
  • path-parameters.adoc
  • request-parts.adoc

이제 이 조각들로 문서를 작성한다.

6. index.adoc 만들기

  1. adoc 파일 작성의 편의를 위해 AsciiDoc 플러그인을 설치한다. (설정 - plugins - 검색 후 설치)
  2. src/docs/asciidoc 디렉토리를 만들고 안에 index.adoc 파일을 만든다.

= REST Docs (글의 제목)

(부제)

:doctype: book
:icons: font
:source-highlighter: highlightjs // 문서에 표기되는 코드들의 하이라이팅을 highlightjs를 사용
:toc: left // toc (Table Of Contents)를 문서의 좌측에 두기
:toclevels: 2
:sectlinks:

[[User-API]]
== User API

[[User-회원가입]]
=== User 회원가입
operation::join/join_success[snippets='http-request,request-fields,http-response,response-fields']

Asciidoc 기본 사용법

  • [[텍스트]]
    • 해당 텍스트에 링크를 건다.
  • operation::디렉토리
    • 문서로 사용할 조각위치를 명시한다.

7. html 파일 생성하기

gradle - build - build를 더블 클릭하여 빌드를 한다.

resources - static - docs 안에 index.html이 생성된다.

8. 결과 확인하기

http://localhost:8080/docs/index.html 으로 접속하면 완성된 문서를 확인할 수 있다.

9. 문서 분리하기

문서 분리할 부분에 커서를 놓고 option + enter (윈도우는 alt + enter?)를 누르면, Extract Include Directive가 나온다.

이를 선택하면 같은 폴더 내로 adoc 파일을 분리해 준다.

📌  추가로 필요한 기능들은 추후 업데이트하겠습니다.

🚫 에러

🚫 테스트코드가 안 됨

Factory method 'mockMvc' threw exception with message: javax/servlet/http/HttpServletResponse

java.lang.ClassNotFoundException: javax.servlet.http.HttpServletResponse

classpath를 못 찾고 있음

⭕️ dependency들 버전을 높여보자

spring security test 버전 5.7.x에서 6.0.x로 올리니 실행되었다.

implementation 'org.springframework.security:spring-security-test:6.0.1'

참고 :

https://velog.io/@backtony/Spring-REST-Docs-적용-및-최적화-하기#2-swagger-vs-rest-docs

https://velog.io/@ililil9482/Restdocs-설정하기

https://techblog.woowahan.com/2597/

profile
훈이야 화이팅
post-custom-banner

0개의 댓글