SIer だけど技術やりたいブログ

Spring REST Docs でAPIドキュメントを作成する

Java Spring

この記事の内容

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コマンド実行のイメージ

  1. テストを実行すると

    • Spring REST Docsが、target/generated-snippetsにasciidocを生成する
  2. テストが終わると、

    • 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

参考