REST ドキュメントの操作

Spring REST Docs を使用すると、Spring、MockMvc、WebTestClient、RestAssured を使用した HTTP API のドキュメント (たとえば、Asciidoc 形式) を生成できます。API のドキュメントを生成するのと同時に、Spring Cloud Contract WireMock を使用して WireMock スタブを生成することもできます。これを行うには、通常の REST ドキュメントテストケースを作成し、@AutoConfigureRestDocs を使用してスタブが REST ドキュメント出力ディレクトリに自動的に生成されるようにします。次の UML 図は、REST ドキュメントのフローを示しています。

rest-docs

次の例では、MockMvc を使用しています。

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class ApplicationTests {

	@Autowired
	private MockMvc mockMvc;

	@Test
	public void contextLoads() throws Exception {
		mockMvc.perform(get("/resource"))
				.andExpect(content().string("Hello World"))
				.andDo(document("resource"));
	}
}

このテストは、target/snippets/stubs/resource.json に WireMock スタブを生成します。すべての GET リクエストを /resource パスと照合します。WebTestClient (Spring WebFlux アプリケーションのテストに使用) での同じ例は次のようになります。

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureWebTestClient
public class ApplicationTests {

	@Autowired
	private WebTestClient client;

	@Test
	public void contextLoads() throws Exception {
		client.get().uri("/resource").exchange()
				.expectBody(String.class).isEqualTo("Hello World")
 				.consumeWith(document("resource"));
	}
}

追加の構成を行わずに、これらのテストは、HTTP メソッドと host と content-length を除くすべてのヘッダーのリクエストマッチャーを備えたスタブを作成します。リクエストをより正確に照合するには (たとえば、POST または PUT の本体と照合するには)、リクエストマッチャーを明示的に作成する必要があります。これにより、次の 2 つの効果が得られます。

  • 指定した方法でのみ一致するスタブを作成します。

  • テストケースのリクエストも同じ条件に一致することをアサートします。

この機能のメインエントリポイントは WireMockRestDocs.verify() で、次の例に示すように、これは document() 便利なメソッドの代わりに使用できます。

import static org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs.verify;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class ApplicationTests {

	@Autowired
	private MockMvc mockMvc;

	@Test
	public void contextLoads() throws Exception {
		mockMvc.perform(post("/resource")
                .content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
				.andExpect(status().isOk())
				.andDo(verify().jsonPath("$.id"))
				.andDo(document("resource"));
	}
}

前述の規約では、id フィールドを持つ有効な POST がこのテストで定義されたレスポンスを受信することを指定しています。チェーンは一緒に .jsonPath() を呼び出して、追加のマッチャーを追加できます。JSON パスが 未知 の場合は、JayWay ドキュメント [GitHub] (英語) を使用すると作業をスムーズに進めることができます。このテストの WebTestClient バージョンには、同じ場所に挿入する同様の verify() 静的ヘルパーがあります。

次の例に示すように、jsonPath および contentType の便利なメソッドの代わりに、WireMock API を使用して、リクエストが作成されたスタブと一致することを確認することもできます。

@Test
public void contextLoads() throws Exception {
	mockMvc.perform(post("/resource")
               .content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
			.andExpect(status().isOk())
			.andDo(verify()
					.wiremock(WireMock.post(urlPathEquals("/resource"))
					.withRequestBody(matchingJsonPath("$.id"))
					.andDo(document("post-resource"))));
}

WireMock API は豊富です。ヘッダー、クエリパラメーター、リクエスト本文は、正規表現および JSON パスによって照合できます。これらの機能を使用すると、より広範囲のパラメーターを持つスタブを作成できます。前述の例では、次の例のようなスタブが生成されます。

post-resource.json
{
  "request" : {
    "url" : "/resource",
    "method" : "POST",
    "bodyPatterns" : [ {
      "matchesJsonPath" : "$.id"
    }]
  },
  "response" : {
    "status" : 200,
    "body" : "Hello World",
    "headers" : {
      "X-Application-Context" : "application:-1",
      "Content-Type" : "text/plain"
    }
  }
}
wiremock() メソッド、jsonPath() および contentType() メソッドのいずれかを使用してリクエストマッチャーを作成できますが、両方のアプローチを使用することはできません。

コンシューマー側では、このセクションの前半で生成した resource.json をクラスパス上で利用できるようにすることができます (たとえば、スタブを JAR として公開するによって)。その後、このドキュメントで前述したように、@AutoConfigureWireMock(stubs="classpath:resource.json") の使用など、さまざまな方法で WireMock を使用するスタブを作成できます。

REST ドキュメントを使用した契約の生成

Spring REST Docs を使用して、Spring Cloud Contract DSL ファイルとドキュメントを生成することもできます。Spring Cloud WireMock と組み合わせて実行すると、契約書と半券の両方が取得されます。

この機能を使用したい理由は何ですか ? コミュニティ内の一部の人々は、DSL ベースの契約定義に移行したい状況について質問しましたが、すでに多くの Spring MVC テストが行われています。この機能を使用すると、後で変更したり、プラグインが見つけられるようにフォルダー (構成で定義) に移動したりできる契約 ファイルを生成できます。

なぜこの機能が WireMock モジュールにあるのか疑問に思われるかもしれません。この機能が存在するのは、契約とスタブの両方を生成することが合理的であるためです。

次のテストを考えてみましょう。

		this.mockMvc
				.perform(post("/foo").accept(MediaType.APPLICATION_PDF).accept(MediaType.APPLICATION_JSON)
						.contentType(MediaType.APPLICATION_JSON).content("{\"foo\": 23, \"bar\" : \"baz\" }"))
				.andExpect(status().isOk()).andExpect(content().string("bar"))
				// first WireMock
				.andDo(WireMockRestDocs.verify().jsonPath("$[?(@.foo >= 20)]")
						.jsonPath("$[?(@.bar in ['baz','bazz','bazzz'])]")
						.contentType(MediaType.valueOf("application/json")))
				// then Contract DSL documentation
				.andDo(document("index", SpringCloudContractRestDocs.dslContract(Maps.of("priority", 1))));

前述のテストでは、前のセクションで示したスタブを作成し、契約とドキュメントファイルの両方を生成します。

この契約は index.groovy と呼ばれ、次の例のようになります。

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    request {
        method 'POST'
        url '/foo'
        body('''
            {"foo": 23 }
        ''')
        headers {
            header('''Accept''', '''application/json''')
            header('''Content-Type''', '''application/json''')
        }
    }
    response {
        status OK()
        body('''
        bar
        ''')
        headers {
            header('''Content-Type''', '''application/json;charset=UTF-8''')
            header('''Content-Length''', '''3''')
        }
        bodyMatchers {
            jsonPath('$[?(@.foo >= 20)]', byType())
        }
    }
}

生成されたドキュメント (この場合は Asciidoc でフォーマットされたもの) には、フォーマットされた契約書が含まれています。このファイルの場所は index/dsl-contract.adoc になります。

優先度属性の指定

メソッド SpringCloudContractRestDocs.dslContract() は、テンプレートに追加の属性を指定できるようにするオプションの Map パラメーターを受け取ります。

これらの属性の 1 つは、次のように指定できる優先フィールドです。

SpringCloudContractRestDocs.dslContract(Map.of("priority", 1))

DSL 契約テンプレートをオーバーライドする

デフォルトでは、契約の出力は default-dsl-contract-only.snippet という名前のファイルに基づいています。

代わりに、次のように getTemplate() メソッドをオーバーライドすることで、カスタムテンプレートファイルを提供できます。

new ContractDslSnippet(){
    @Override
    protected String getTemplate() {
        return "custom-dsl-contract";
    }
}));

上の例はこの行を示しています

.andDo(document("index", SpringCloudContractRestDocs.dslContract()));

を次のように変更する必要があります。

.andDo(document("index", new ContractDslSnippet(){
                            @Override
                            protected String getTemplate() {
                                return "custom-dsl-template";
                            }
                        }));

テンプレートは、クラスパスでリソースを探すことによって解決されます。次の場所が順番にチェックされます。

  • org/springframework/restdocs/templates/${templateFormatId}/${name}.snippet

  • org/springframework/restdocs/templates/${name}.snippet

  • org/springframework/restdocs/templates/${templateFormatId}/default-${name}.snippet

上記の例では、custom-dsl-template.snippet という名前のファイルを src/test/resources/org/springframework/restdocs/templates/custom-dsl-template.snippet に配置する必要があります。