Spring REST DocsとSpring Cloud Contractを連携して使ってみる
はじめに
Spring REST DocsはSpringのRestFullアプリケーションにおけるWebAPIのインターフェースをドキュメント化する際のサポートをしてくれます。Spring MVC Testをもとにスニペットを自動生成し、Asciidoctorの形式で出力してくれます。
また、Spring Cloud ContractはConsumer Driven Contractのサポートを提供しているプロジェクトです。Spring Cloud Contractに関しては以前にブログを書いたので良ければこちらをご覧ください。
この2つは組み合わせて利用することが可能なようなので、試してみたいと思います。
組み合わせることでなにが変わるのか?
端的に言うと、Spring Cloud Contractで書いていた契約のYamlファイルを書く必要がなくなるみたいです。その分テストを自分で書くことになるのでその面だけで考えると一長一短みたいなところはあるかもしれないです。ただ、全体として考えると、テストを記述することでCDCのサポートを受けれるようになり、かつAPIがヒューマンリーダブルな形でドキュメント化されるようになります。
やってみる
実行環境
実行環境は以下の通り
$ java --version openjdk 15.0.1 2020-10-20 OpenJDK Runtime Environment (build 15.0.1+9-18) OpenJDK 64-Bit Server VM (build 15.0.1+9-18, mixed mode, sharing) $ mvn --version Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 15.0.1, vendor: Oracle Corporation, runtime: /home/someone/.sdkman/candidates/java/15.0.1-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-52-generic", arch: "amd64", family: "unix"
プロジェクトはSpring Iniializrを使って、以下の設定で作成します
また、CDCのConsumer側はSpring Cloud Contractの使い方と変わらないため、Spring Cloud ContractのProducer側のみを作成します。
pomを軽く眺めてみる
作成されたプロジェクトのpomを軽く眺めてみると必要な依存が入っているのといくつかのプラグインが設定されています。
<plugins> <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>2.2.4.RELEASE</version> <extensions>true</extensions> <configuration> <testFramework>JUNIT5</testFramework> </configuration> </plugin> <plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>1.5.8</version> <executions> <execution> <id>generate-docs</id> <phase>prepare-package</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <backend>html</backend> <doctype>book</doctype> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-asciidoctor</artifactId> <version>${spring-restdocs.version}</version> </dependency> </dependencies> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins>
asciidocとspring−cloud-contractのプラグインが入れられているのが確認できます。
Pom等に設定を追加する
2つを連携されるためにいくつか設定を行なう必要があります。
PomにContractのテストをスキップする設定を追加する
前述の通り、REST DocとCloud Contractを連携される場合、Yamlなどの契約を書く必要がありません。つまり、Cloud Contractが自動生成するテスト実行する必要がないので、スキップする設定を追加してやる必要があります。
ドキュメントによると、ユーザープロパティにspring.cloud.contract.verifier.skip
をtrueで設定してやることでテストをスキップできるようなので設定を記述します。
<properties> <java.version>15</java.version> <spring-cloud.version>Hoxton.SR8</spring-cloud.version> <spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip> </properties>
Spring Cloud Contract Pluginを削除する(追記)
このブログを投稿した際に以下のようなアドバイスをSpring Cloud Contract のAutherであるMarcin Grzejszczakさんからいただきました。
Thank you for telling me this, Mr. Marcin Grzejszczak!!
Nice article! BTW if you're using Spring Cloud Contract with Rest Docs you don't need to add the maven contract plugin cause you will not generate the tests nor will you package the stubs automatically.
— Marcin Grzejszczak (@MGrzejszczak) November 2, 2020
Contract とREST Docsを連携させる場合はテストもスタブも自動生成するわけで無いので、このプラグインは必要ないとのことです。なので消してしまって大丈夫です。
Stubの作成のための設定を追加する
StubのJarを作成するためにassembly pluginを入れて設定を追加してやる必要があります。まず、以下のプラグインをPomに追加します。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <executions> <execution> <id>stub</id> <phase>prepare-package</phase> <goals> <goal>single</goal> </goals> <inherited>false</inherited> <configuration> <attach>true</attach> <descriptors>./src/assembly/stub.xml</descriptors> </configuration> </execution> </executions> </plugin>
次にサンプルを参考にstub.xml
を作成し${project_root}/src/assembly/
配下に配置します。
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd"> <id>stubs</id> <formats> <format>jar</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <fileSets> <fileSet> <directory>src/main/java</directory> <outputDirectory>/</outputDirectory> <includes> <include>**dev/hirooka/model/*.*</include> </includes> </fileSet> <fileSet> <directory>${project.build.directory}/classes</directory> <outputDirectory>/</outputDirectory> <includes> <include>**dev/hirooka/model/*.*</include> </includes> </fileSet> <fileSet> <directory>${project.build.directory}/snippets/stubs</directory> <outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings</outputDirectory> <includes> <include>**/*</include> </includes> </fileSet> <fileSet> <directory>./target/generated-snippets/contracts</directory> <outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/contracts</outputDirectory> <includes> <include>**/*.groovy</include> </includes> </fileSet> </fileSets> </assembly>
ここで、ポイントは<directory>./target/generated-snippets/contracts</directory>
のところで、これは自動生成される契約のDSLを指定してやる必要があります。
デフォルトではtarget配下にcontract
ディレクトリが作成されそこにgroovyが出力されるのでそこを指定します。
<plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>2.2.4.RELEASE</version> <extensions>true</extensions> <configuration> <testFramework>JUNIT5</testFramework> <packageWithBaseClasses>dev.hirooka</packageWithBaseClasses> </configuration> </plugin>
spring-cloud-contract-wiremockの依存を追加する
Spring Cloud Contract Rest Docsのインテグレーションを使うためにはpomに以下の依存を追加する必要があります。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-wiremock</artifactId> <scope>test</scope> </dependency>
こいつを追加することで、Rest Docからスタブを生成することができるようになります。
ベースとなるテストと実装を書く
実装に入る前に先にテストを記述しておきます。
テストはJunit5とMockMVC
を使って記述します。Spring REST Docsは他にも、WebTestClient
やREST Assured
でも利用できます。
@SpringBootTest class HelloControllerTest { private MockMvc mockMvc; @BeforeEach public void setUp(WebApplicationContext context) { this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); } @Test void HelloControllerのテスト() throws Exception { mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.hello").value("world")); } }
ここまでは一般的なテストですね次に実装を書きます。
@RestController public class HelloController { @GetMapping("/") public Map<String, String> hello() { return Map.of("hello", "world"); } }
実装もごくごく一般的なコントローラーです。
テストを実行すると成功します。
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.282 s - in hirooka.dev.cdcanddocs.controller.HelloControllerTest
REST Docsを追加する
ベースとなる実装ができたところでテストにREST Docsの設定を入れていきます。
先程の@BeforeEach
で書いたMockMvc
の設定とテスト自体を少しいじります。
@SpringBootTest @ExtendWith(RestDocumentationExtension.class) class HelloControllerTest { private MockMvc mockMvc; @BeforeEach public void setUp( WebApplicationContext context, RestDocumentationContextProvider provider ) { this.mockMvc = MockMvcBuilders.webAppContextSetup(context) .apply(documentationConfiguration(provider)) .build(); } @Test void HelloControllerのテスト() throws Exception { mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.hello").value("world")) .andDo(document("index")); } }
僕自身がRestDocsを使うのははじめてなので少しだけ突っ込んでまとめようと思います。
まず、最初に@ExtendWith(RestDocumentationExtension.class)
を追加しています。このエクステンションを入れることで、@BforeEach
の引数でRestDocumentationContextProviderを受け取ることができます。このクラスはRestDocumentationContext
へのアクセスを提供します。
次に@BeforeEach
にでMockMvc
に設定を追加しています。 MockMvcRestDocumentationのdocumentationConfiguration()
スタティックメソッドを使ってMockMvc
にMockMvc用のRest Docs拡張である MockMvcRestDocumentationConfigurerを登録しています。ここまでで、事前準備は完了です。
次にTestクラスに着目すると、最後のandDo()
で MockMvcRestDocumentationのdocumennt()
スタティックメソッドを呼び出しています。このスタティックメソッドは第一引数にドキュメントの識別子と第二引数に可変引数としてSnippetインターフェースの実装を受け取ります。この実装は出力ドキュメントに対して付加情報を追加する際に利用します。
mvn test
を実行するとtarget/generated-snippets/index/*.adoc
にデフォルトでは以下の画像の6種類のasciidocが出力されます。
結果の出力はMarkDownなどでも行えるようですが、ここでは深く触れないよ言うにします。
Contractと連携する。
AsciiDocの出力まで終わったところで、Spring Cloud Contractとの連携を行ってみようと思います。
テストコードを以下のように修正します。
@Test void HelloControllerのテスト() throws Exception { mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.hello").value("world")) .andDo(document("index", SpringCloudContractRestDocs.dslContract())); }
document()
の引数にSpringCloudContractRestDocs.dslContract()
を渡すことで契約のDSLとスタブが自動生成されるようになります。
mvn clean install
を実行するとtarget配下が以下のように生成されることがわかります。
Consumer側からのStubの利用
いくつか方法がありますが@AutoConfigureStubRunner
を使った方法をこちらにまとめてますのでそちらを参照してください。
リクエストをバリデートするような実装に変えてみる
今までは、リクエストに対して固定値を返すだけのシンプルな実装でしたが、リクエストのパターンによって返すものを変えるような実装に変えてみます。 例えば以下のようなストーリーを想定します。
Rest DocsとCloud Contractの連携でリクエストのヴァリファイを行なうスタブを作成する場合、WireMockRestDocs
を利用します。
@Test void HelloControllerのOKテスト() throws Exception { mockMvc.perform(post("/") .contentType(MediaType.APPLICATION_JSON) .content("{\"value\": 10}")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.status").value("OK")) .andDo( WireMockRestDocs.verify() .contentType(MediaType.APPLICATION_JSON) .jsonPath("$[?(@.value >= 10)]")) .andDo(document("okPattern", SpringCloudContractRestDocs.dslContract())); } @Test void HelloControllerのNGテスト() throws Exception { mockMvc.perform(post("/") .contentType(MediaType.APPLICATION_JSON) .content("{\"value\": 9}")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.status").value("NG")) .andDo( WireMockRestDocs.verify() .contentType(MediaType.APPLICATION_JSON) .jsonPath("$[?(@.value < 10)]")) .andDo(document("ngPattern", SpringCloudContractRestDocs.dslContract())); }
同じくmvn clean install
をすればstubが生成されます。