この記事の内容
Spring REST Docsを利用すれば、テストコードから以下のようなAPIドキュメントを作成できるようになる。
Spring REST Docs とは
RESTfulなサービスのドキュメント作成を支援するプロダクト。
テストコードをもとにasciidoc形式のドキュメントを生成する。
テストをパスした内容しかドキュメント化させないので、正確なドキュメント作成ができる。
生成したasciidocはAsciidoctorでhtml,pdfなどに変換できる。
(自分で書いたasciidocとも統合できる)
テストコードはデフォルトではJUnit + Spring MVC Testをサポート。
(TestNGやREST Assuredなどの他のライブラリも対応している)
サンプル
サンプル書いた。
kimullaa/spring-restdocs-samplegithub.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
ライフサイクルってなに?という方には下記も参考になると思います。
Maven 「mvn checkstyle:checkstyle」みたいなコマンドと「mvn test」みたいなコマンドのちがい - SIerだけど技術やりたいブログ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