この記事の内容
Spring REST Docsを利用すれば、テストコードから以下のようなAPIドキュメントを作成できるようになる。
Spring REST Docs とは
RESTfulなサービスのドキュメント作成を支援するプロダクト。
テストコードをもとにasciidoc形式のドキュメントを生成する。
テストをパスした内容しかドキュメント化させないので、正確なドキュメント作成ができる。
生成したasciidocはAsciidoctorでhtml,pdfなどに変換できる。
(自分で書いたasciidocとも統合できる)
テストコードはデフォルトではJUnit + Spring MVC Testをサポート。
(TestNGやREST Assuredなどの他のライブラリも対応している)
サンプル
サンプル書いた。
github.com
使い方
まずは、SpringInitializrからひな形を生成する。
mavenのpom.xmlにversionを指定する
SpringBootの1.3.6.RELEASEはspring-mvc-restdocバージョンが古いので、spring-boot-starter-parent で指定されているspring-restdocsのversionを上書きする。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <version>1.1.0.RELEASE</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-core</artifactId> <version>1.1.0.RELEASE</version> <scope>test</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-core</artifactId> <scope>test</scope> </dependency> </dependencies>
Asciidoctorでasciidocからhtmlを生成する
以下を参考にpom.xmlにAsciidoctorの設定をする。
https://github.com/asciidoctor/asciidoctor-maven-plugin
<plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>1.5.2</version> <configuration> <!-- 変換元のadocのディレクトリ --> <sourceDirectory>${snippetsDirectory}</sourceDirectory> <!-- 変換先のhtmlのディレクトリ --> <outputDirectory>${docDirectory}</outputDirectory> </configuration> <executions> <execution> <id>asciidoc-to-html</id> <phase>prepare-package</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <backend>html5</backend> <doctype>book</doctype> <sourceHighlighter>coderay</sourceHighlighter> <preserveDirectories>true</preserveDirectories> <sourceDocumentName>index.adoc</sourceDocumentName> <!-- asciidocからgenerated-snippetsを参照するときに使う変数定義--> <attributes> <snippets>${snippetsDirectory}</snippets> </attributes> </configuration> </execution> </executions> </plugin>
生成したhtmlをjarに含める
<plugin> <artifactId>maven-resources-plugin</artifactId> <version>2.7</version> <executions> <execution> <id>copy-resources</id> <phase>prepare-package</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory> ${project.build.outputDirectory}/static/docs </outputDirectory> <resources> <resource> <directory> ${docDirectory} </directory> </resource> </resources> </configuration> </execution> </executions> </plugin>
各pluginを実行するphase
上2つのpluginはtestが通ってpackageが始まる前のフェーズ(prepare-pachage)に実行する。そのため、テストが失敗した場合はasciidocは生成されない。
mavenのライフサイクルの詳細はここ。
https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Lifecycle_Reference
ライフサイクルってなに?という方には下記も参考になると思います。
kimulla.hatenablog.com
自作のasciidocと統合したい場合
htmlに変換する前に、自作したasciidocをgenerated-snippetsディレクトリにコピーする。
<plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-adoc</id> <phase>validate</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${snippetsDirectory}</outputDirectory> <resources> <resource> <directory>src/main/asciidoc</directory> </resource> </resources> </configuration> </execution> </executions> </plugin>
mavenコマンド実行のイメージ
- テストを実行すると
- Spring REST Docsが、target/generated-snippetsにasciidocを生成する
- テストが終わると、
- maven-resources-pluginが、src/main/adocからtarget/generated-snippetsにコピーする
- Asciidoctorが、target/generated-docsにhtmlを生成する
テストコード
SpringMVC Testを使った単体テストを書く。
ドキュメントをどの単位で出力するのかはalwaysDoやandDoに記載されたdocument(...)で制御する。今回はテストメソッドごとに出力している。
同じ出力先を複数テストケースで指定すると、同名ファイルは上書きされる。
public class GreetingControllerTest { private MockMvc mockMvc; // asciidocの出力先ディレクトリ @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); @Before public void setUp() { this.mockMvc = MockMvcBuilders .standaloneSetup(new GreetingController()) .apply(documentationConfiguration(restDocumentation)) // 全てのテストケースで出力するドキュメント // メソッドごとにasciidocを生成する // リクエストの説明をドキュメントに出力する .alwaysDo(document("greeting/{method-name}", requestParameters( parameterWithName("hour") .description("時刻: 必須入力(0-24)") ))) .build(); } @Test public void doc() throws Exception { this.mockMvc.perform(get("/greeting").param("hour", "24")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.greeting").value("good night")) // レスポンス結果をドキュメントに出力する .andDo(document("greeting/{method-name}", responseFields( fieldWithPath("greeting").type(JsonFieldType.STRING) .description("hourが0-4,16-24の場合はgood night,5-15の場合はgood morning") ))); } @Test public void 入力値範囲外_下限越え() throws Exception { this.mockMvc.perform(get("/greeting").param("hour", "-1")) .andExpect(status().isBadRequest()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)); } .. }
自作のasciidocドキュメントを作る
今のままだと以下のようなasciidocが出力されるだけなので、
これらをまとめて1つのドキュメントにできるようにasciidocを作成する。
- target/generated-snippets/greeting/doc/curl-request.adoc
- target/generated-snippets/greeting/doc/http-request.adoc
- target/generated-snippets/greeting/doc/http-response.adoc
- target/generated-snippets/greeting/doc/httpie-request.adoc
- target/generated-snippets/greeting/doc/response-fields.adoc
- target/generated-snippets/greeting/doc/request-parameters.adoc
- target/generated-snippets/greeting/入力値範囲外_下限越え/curl-request.adoc
- target/generated-snippets/greeting/入力値範囲外_下限越え/httpie-request.adoc
- target/generated-snippets/greeting/入力値範囲外_下限越え/http-request.adoc
- target/generated-snippets/greeting/入力値範囲外_下限越え/http-response.adoc
- target/generated-snippets/greeting/入力値範囲外_下限越え/request-parameters.adoc
src/main/adoc にindex.adocを作る。
今回は正常系だけをHTMLドキュメントにするので、入力値範囲外_下限越えのドキュメントは使わない。
= API Document [[greeting]] == Greeting 時刻に応じたあいさつを返す === Request Parameter include::{snippets}/greeting/doc/request-parameters.adoc[] === Response Fields include::{snippets}/greeting/doc/response-fields.adoc[] === Example request include::{snippets}/greeting/doc/curl-request.adoc[] === Example response include::{snippets}/greeting/doc/http-response.adoc[]
最終的に生成されるドキュメント(html)
target/generated-docsに、いい感じのドキュメントが生成される。
テストと反するドキュメントを書くと…
レスポンスのフィールドを実際の値から以下のように変えると…
fieldWithPath("greetingaa").type(JsonFieldType.STRING)
エラーになる。
Tests run: 9, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.997 sec <<< FA ILURE! - in com.example.controllers.GreetingControllerTest doc(com.example.controllers.GreetingControllerTest) Time elapsed: 0.063 sec << < ERROR! org.springframework.restdocs.snippet.SnippetException: The following parts of th e payload were not documented: { "greeting" : "good night" } Fields with the following paths were not found in the payload: [greetingaa] at org.springframework.restdocs.payload.AbstractFieldsSnippet.validateFi eldDocumentation(AbstractFieldsSnippet.java:158) at org.springframework.restdocs.payload.AbstractFieldsSnippet.createMode l(AbstractFieldsSnippet.java:97) at org.springframework.restdocs.snippet.TemplatedSnippet.document(Templa tedSnippet.java:64) at org.springframework.restdocs.generate.RestDocumentationGenerator.hand le(RestDocumentationGenerator.java:196) at org.springframework.restdocs.mockmvc.RestDocumentationResultHandler.h andle(RestDocumentationResultHandler.java:54) at org.springframework.test.web.servlet.MockMvc$1.andDo(MockMvc.java:177 ) at com.example.controllers.GreetingControllerTest.doc(GreetingController Test.java:85) ... Results : Tests in error: GreetingControllerTest.doc:85 ≫ Snippet The following parts of the payload w r... Tests run: 10, Failures: 0, Errors: 1, Skipped: 0
所感
良いところ
- テストケースをパスしないとドキュメントが作れないので、ある程度コードとドキュメントとの整合性は保てそう
- asciidocなのでカスタマイズしやすそう
- PDFやHTMLなどの色々な種類のドキュメントを生成できる
悪いところ
- テストケースをパスしないとドキュメントが作れないので、設計ドキュメントにはできなそう
検討しないといけないこと
- INPUTごとに異なる返却値を返す場合、細かな内部仕様はテストケースから生成できるのか(難しそう)
結論
やっぱりテストケースから仕様を生成するよりも、まずは仕様をまとめたいので、Swaggerみたいなツールのほうがいいかな。
(Springアノテーションからドキュメント生成できるSwagger拡張のSpringFoxを試してみたい)
https://github.com/springfox/springfox