가급적이면 모든 문서들은 자동화 해야겠다는 취지 + 별도 관리하지 않도록 하기위해 이것저것 알아보다가 이걸로 하기로 결정했습니다. 무엇보다 테스트를 하지 않으면 문서생성이 안된다는 점이 맘에 들었습니다.(실패하면 문서생성 안됨)
반대로 단점이 있다면 실행한 메서드별로 폴더생성되고, 동일한 이름으로 생성할 경우 마지막실행된 것으로 덮어씌웁니다. 그래서 API가 추가될때마다 문서도 추가/수정 해줘야 한다는 문제점이 있습니다. 그냥 엑셀에 관리해도 괜찮지 않을까 생각도 드네요.
여기서는 테스트 코드를 spock 를 활용(groovy 문법) 했고 gradle을 사용했습니다. 그리고 API를 호출하는 것으로는 REST Assured을 사용했습니다. Spring Rest Docs 공식문서에 REST Assured 로 작성된 테스트가 있어 사용했습니다.
Spring Rest Docs + Spock + Rest Assure
- build.gradle 설정
plugins, dependencies, ext, test, asciidoctor, build, copyDocument, bootJar 등 설정합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | buildscript{ ... } // Spring Rest Docs plugins { id "org.asciidoctor.convert" version "1.5.3" } ... // SPOCK 설정 apply plugin: 'groovy' dependencies{ ... // Spock testCompile('org.spockframework:spock-core:1.1-groovy-2.4') testCompile('org.spockframework:spock-spring:1.1-groovy-2.4') // Spring Rest Docs // asciidoc asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.2.RELEASE' testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.2.RELEASE' testCompile('io.rest-assured:rest-assured:3.0.2') // for rest assured testCompile('org.springframework.restdocs:spring-restdocs-restassured') // for rest assured } // Spring Rest Docs ext { snippetsDir = file('build/generated-snippets') } test { // 만약 application 안에 spring.profiles=test 설정이 되어있는게 있다면 그 설정을 참조합니다. // 다만 아래와 같이 설정하여 application.yml 을 구성한다면 매번 테스트마다 VM options 에 다음의 설정을 넣어주어야 하니 주의해야합니다. (-Dspring.profiles.active=test) environment SPRING_PROFILES_ACTIVE: environment.SPRING_PROFILES_ACTIVE ?: "test" outputs.dir snippetsDir } asciidoctor { attributes 'snippets': snippetsDir inputs.dir snippetsDir dependsOn test } // build/asciidoc 폴더에 생성된 html 파일을 static 으로 복사하여 localhost/docs/*.html 로 볼 수 있습니다. task copyDocument(type: Copy) { dependsOn asciidoctor from file("build/asciidoc/html5/") into file("src/main/resources/static/docs") } build { dependsOn copyDocument } bootJar { dependsOn asciidoctor from ("${asciidoctor.outputDir}/html5") { into 'static/docs' } } | cs |
- Controller 생성 및 테스트 작성
간단한 테스트를 위해 컨트롤러를 생성합니다.
# UserController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * 테스트 컨트롤러 */ @RestController public class UserController { /** * GET 방식 호출 * @param name * @return */ @GetMapping("/user-get") public Map user_get(@RequestParam String name){ User user = new User(); user.setName(name); user.setAge(24); user.setAddress("서울"); user.setComment("GET 호출 입니다"); Map result = new HashMap<>(); result.put("code", "0000"); result.put("message", "OK"); result.put("data", user); return result; } /** * POST 방식 호출 */ @PostMapping("/user-post") public Map user_post(@RequestBody User user){ user.setComment("POST 호출 입니다"); Map result = new HashMap<>(); result.put("code", "0000"); result.put("message", "OK"); result.put("data", user); return result; } } | cs |
# User.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * 테스트 컨트롤러 */ @RestController public class UserController { /** * GET 방식 호출 * @param name * @return */ @GetMapping("/user-get") public Map user_get(@RequestParam String name){ User user = new User(); user.setName(name); user.setAge(24); user.setAddress("서울"); user.setComment("GET 호출 입니다"); Map result = new HashMap<>(); result.put("code", "0000"); result.put("message", "OK"); result.put("data", user); return result; } /** * POST 방식 호출 */ @PostMapping("/user-post") public Map user_post(@RequestBody User user){ user.setComment("POST 호출 입니다"); Map result = new HashMap<>(); result.put("code", "0000"); result.put("message", "OK"); result.put("data", user); return result; } } | cs |
부트를 실행한 후 postman 등으로 테스트해봅니다.
정상작동 됩니다.
이제 테스트를 작성해보겠습니다.
- 테스트 작성
테스트를 작성하기 전에 문서에 적힐 test URL을 환경변수로 저장해둡니다.
# application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | server: port: 8080 # Spring docs 설정하는데 사용. test.server.http.scheme: http test.server.http.host: test-server.com test.server.http.port: 8080 spring: profiles: active: local --- spring: profiles: dev | cs |
테스트 클래스를 작성합니다. 여기서는 SPOCK으로 테스트코드를 작성했습니다.
# UserControllerTest.groovy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | import io.restassured.builder.RequestSpecBuilder import io.restassured.http.ContentType import io.restassured.response.Response import io.restassured.specification.RequestSpecification import org.junit.Rule import org.springframework.beans.factory.annotation.Value import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.web.server.LocalServerPort import org.springframework.restdocs.JUnitRestDocumentation import org.springframework.restdocs.payload.JsonFieldType import org.springframework.test.context.ContextConfiguration import spock.lang.Specification import static io.restassured.RestAssured.given import static org.springframework.restdocs.operation.preprocess.Preprocessors.* import static org.springframework.restdocs.payload.PayloadDocumentation.* import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName import static org.springframework.restdocs.request.RequestDocumentation.requestParameters import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // @Autowired 등 옵션을 설정하는것을 쓰기 위해선 ContexntConfiguration 이 필요 @ContextConfiguration(classes = SpringDocsApplication) class UserControllerTest extends Specification { @Rule public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() private RequestSpecification spec @LocalServerPort private int port @Value('${test.server.http.scheme}') String httpScheme; @Value('${test.server.http.host}') String httpHost; @Value('${test.server.http.port}') int serverPort; void setup() { this.spec = new RequestSpecBuilder() .addFilter(documentationConfiguration(this.restDocumentation)) .build() } /** * Get 통신 */ def "user-get"(){ expect: Response res = given(this.spec) .contentType(ContentType.JSON) .accept(ContentType.JSON) // build/generated-snippets 폴더 내 API 정보가 쌓이는 폴더 지정(이후 웹으로 보여줄때 사용) // - {method-name} 은 실행중인 메서드 이름 .filter(document("user/{method-name}", preprocessRequest(modifyUris() .scheme(httpScheme) .host(httpHost) // 만약 포트가 없다면 removePort() 를 사용하거나 포트번호에 80을 주면 됩니다. // .removePort() .port(serverPort), prettyPrint()), preprocessResponse(prettyPrint()), // Request Fields (Get 호출) // - 표 형태로 보여진다. requestParameters( parameterWithName("name").description("이름") ), // 결과값에서 하나라도 빠지면 에러발생. // - null 로 리턴된다 하더라도 등록해두어야 함 // - null 이 허용된다면 optional() 을 꼭 주어야 함. responseFields( fieldWithPath("code").type(JsonFieldType.STRING).description("코드").ignored(), fieldWithPath("message").type(JsonFieldType.STRING).description("메세지").ignored(), // data 오브젝트 안의 데이터 fieldWithPath("data.name").type(JsonFieldType.STRING).description("유저 명"), fieldWithPath("data.age").type(JsonFieldType.NUMBER).description("나이").optional(), fieldWithPath("data.address").type(JsonFieldType.STRING).description("주소").optional(), fieldWithPath("data.comment").type(JsonFieldType.STRING).description("유저 메세지"), ) )) // 실제 전송할 때 쓸 파라미터 설정 .param("name","철수") // 테스트 URL // - port 는 Random으로 띄워지기 때문에(WebEnvironment.RANDOM_PORT) // 위에 @LocalServerPort 을 이용해 테스트에 쓰인 port를 사용하여 호출 .when().port(this.port).get("/user-get") // 1) 통신 결과 res.then().assertThat().statusCode(200) // 2) 결과 값 def result = res.then().extract().response().asString() println "결과=> " + result } /** * Post 통신 */ def "user-post"(){ given: Map testParam = new HashMap(); testParam.put("name", "홍길동") testParam.put("age", 20) testParam.put("addr", "서울") expect: Response res = given(this.spec) .contentType(ContentType.JSON) .accept(ContentType.JSON) // build/generated-snippets 폴더 내 API 정보가 쌓이는 폴더 지정(이후 웹으로 보여줄때 사용) // {method-name} 은 실행중인 메서드 이름 .filter(document("user/{method-name}", ost("/user-post") // 1) 통신 결과 res.then().assertThat().statusCode(200) // 2) 결과 값 def result = res.then().extract().response().asString() println "결과=> " + result } } | cs |
.filter(document(...)) 에 들어가는 내용들은 문서자동화를 할떄 참조하는 옵션들입니다.
@ document(param1, ...): param1에 들어가는 항목으로 폴더가 생성됩니다. 메서드마다 이름을 달리해주지 않으면 마지막으로 실행된 결과값으로 덮어쓰게 됩니다. {method-name} 으로 설정하면 실행하는 메서드이름으로 폴더가 자동생성 됩니다.
@ preprocessRequest: 파일을 생성할때 url, port 등을 설정할 수 있습니다. 만약 설정되어 있지 않다면 localhost:[가상포트번호] 로 등록됩니다.(대상파일: curl-request.adoc, httpie-request.adoc)
(등록하지 않은 경우) $ curl 'http://localhost:54683/user-get?name=%EC%B2%A0%EC%88%98' -i -X GET \
-H 'Accept: application/json, application/javascript, text/javascript' \
-H 'Content-Type: application/json; charset=UTF-8'
(등록한 경우) $ curl 'http://test-server.com:8080/user-get?name=%EC%B2%A0%EC%88%98' -i -X GET \
-H 'Accept: application/json, application/javascript, text/javascript' \
-H 'Content-Type: application/json; charset=UTF-8'
@ requestParameters: GET 호출 시 전송하는 파라미터 정보를 등록합니다. '파라미터명, 설명'으로 구성됩니다. requestParameters(parameterWithName()) 를 사용합니다.
@ requestFields: POST 호출 시 전송하는 파라미터 정보를 등록합니다. '파라미터명', '데이터 타입', '설명'으로 구성됩니다. requestFields(fieldWithPath())를 사용합니다.
@ responseFields: response-fields asciidoc 파일을 생성합니다. responseFields(fieldWithPath())를 사용합니다
@ requestParameters, requestFields, responseFields 이 세가지는 누락되는 필드가 있으면 테스트가 깨집니다. 그리고 만약 null을 허용하려면 .optional() 를 추가해야 합니다.
예) fieldWithPath("data.age").type(JsonFieldType.NUMBER).description("나이").optional(),
@ Spring Rest Docs 문서에 필드 설명을 제거하려면 .ignored() 를 추가하면 됩니다.
예) fieldWithPath("code").type(JsonFieldType.STRING).description("코드").ignored(),
./gradlew build 를 수행하면 다음과 같이 asciidoc의 파일이 생성됩니다.
- Docs 문서파일 생성
해당 문서는 asciidoc 문법을 사용합니다.
src/docs/asciidoc 안에 index.adoc 파일을 생성합니다. 그리고 test 에서 작성한것은 user/메서드명 으로 했기에 user라는 폴더도 만들고 그 안에도 index.adoc 파일을 생성합니다.
# src/docs/asciidoc/index.adoc 파일
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | ifndef::snippets[] :snippets: ../../../build/generated-snippets endif::[] :doctype: book :icons: font :source-highlighter: highlightjs :toc: left :toclevels: 4 :sectlinks: :site-url: /stock-memo/build/asciidoc/html5/ = Rest Docs Sample Document == 소개 Rest Docs Sample Document == 공통 === Domain |=== | 환경 | domain | 테스트서버 | test-server.com:8080.com | 운영서버 | live-server.com |=== === 공통 Response Body |=== | field | 설명 | `code` | 응답 코드 | `message` | 응답 메세지 | `data` | 실 데이터 |=== == 응답 코드 link:code.html[링크] //== API 목록 //link:api-list.html[링크] == API 목록 |=== | API 주소 | 설명 | link:user/index.html#_유저_정보_조회_get[/hello] | 유저정보 조회(GET) | link:user/index.html#_유저_정보_조회_post[/hello2] | 유저정보 조회(POST) |=== | cs |
# src/docs/asciidoc/user/index.adoc 파일
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | ifndef::snippets[] :snippets: ../../../build/generated-snippets endif::[] :doctype: book :icons: font :source-highlighter: highlightjs :toc: left :toclevels: 4 :sectlinks: :site-url: /stock-memo/build/asciidoc/html5/ = User API == link:../index.html[> 홈으로] //link:../api-list.html[API 목록으로] == 설명 유저 정보 조회 API를 테스트 합니다. == 유저 정보 조회(GET) === Request CURL: include::{snippets}/user/user-get/curl-request.adoc[] Request Parameters: include::{snippets}/user/user-get/request-parameters.adoc[] Request HTTP Example: include::{snippets}/user/user-get/http-request.adoc[] === Response Response Fields: include::{snippets}/user/user-get/response-fields.adoc[] Response HTTP Example: include::{snippets}/user/user-get/http-response.adoc[] == 유저 정보 조회(POST) === Body CURL: include::{snippets}/user/user-post/curl-request.adoc[] Request Parameters: include::{snippets}/user/user-post/request-fields.adoc[] include::{snippets}/user/user-post/request-body.adoc[] Request HTTP Example: include::{snippets}/user/user-post/http-request.adoc[] === Response Response Fields: include::{snippets}/user/user-post/response-fields.adoc[] Response HTTP Example: include::{snippets}/user/user-post/http-response.adoc[] | cs |
./gradlew build 를 실행하면 생성한 파일폼을 기준으로 build/asciidoc/html5 폴더아래에 생성됩니다. 그리고 위에 build.gradle 파일에서 설정한 것을 통해 src/main/resources/static/docs 폴더에 복사됩니다.
1 2 3 4 5 6 7 | // 이부분 task copyDocument(type: Copy) { dependsOn asciidoctor from file("build/asciidoc/html5/") into file("src/main/resources/static/docs") } | cs |
해당경로에 가서 html 파일을 브라우저 통해 열어보거나 혹은 springboot를 실행시켜 localhost:8080/docs/index.html 에 접속해봅니다. 다음 그림처럼 메인화면이 보이는 것을 확인할 수 있습니다.
테스트 한 /user-get 를 문서에서 확인해보겠습니다.
메인 화면에서 'API 목록' 항목으로 가서 'API 주소'를 클릭하거나 아래의 경로로 입력해 들어가면 확인할 수 있습니다.
http://localhost:8080/docs/user/index.html
참고링크:
Gradle Multi Module에서 Spring Rest Docs 사용하기
https://jojoldu.tistory.com/294
Spring REST Docs
https://docs.spring.io/spring-restdocs/docs/2.0.2.RELEASE/reference/html5/
REST Assured
Asciidoc 기본 사용법
https://narusas.github.io/2018/03/21/Asciidoc-basic.html
관련 코드는 Github에 올려두었습니다.
https://github.com/lemontia/SpringRestDocs-Spock
'공부 > 프로그래밍' 카테고리의 다른 글
[maven] install 시 class 파일이 생성되지 않을 때 (0) | 2018.11.28 |
---|---|
[python-pip] mysqlclient 설치 중 에러날때(mysql.h) (4) | 2018.11.09 |
[docker] MariaDB replication(master-slave) 설정 (2) | 2018.10.25 |
[docker] MariaDB + 로컬에 데이터저장소 연결 (0) | 2018.10.24 |
CI/CD, 서버패턴 (0) | 2018.10.23 |
댓글