Working with REST Docs

通过编写 REST Docs 测试用例并使用 @AutoConfigureRestDocs,可以同时生成 API 文档和 WireMock 存根。REST Docs 测试用例可以验证请求并生成存根,并通过与 Spring Cloud Contract 配合使用来生成合约。

本文提供了使用 MockMvc 和 WebTestClient 编写 REST Docs 测试用例的示例,还展示了如何使用 WireMockRestDocs.verify() 来精确匹配请求,并说明了生成合约以及覆盖默认 DSL 合约模板的方法。

您可以使用 Spring REST Docs 为使用 Spring MockMvc、WebTestClient 或 RestAssured 的 HTTP API 生成文档(例如,采用 Asciidoc 格式)。同时为您的 API 生成文档时,还可以使用 Spring Cloud Contract WireMock 生成 WireMock 存根。为此,编写常规的 REST Docs 测试用例并使用 @AutoConfigureRestDocs 在 REST Docs 输出目录中自动生成存根。下面的 UML 图显示了 REST Docs 流程:

You can use Spring REST Docs to generate documentation (for example, in Asciidoc format) for an HTTP API with Spring MockMvc, WebTestClient, or RestAssured. At the same time that you generate documentation for your API, you can also generate WireMock stubs by using Spring Cloud Contract WireMock. To do so, write your normal REST Docs test cases and use @AutoConfigureRestDocs to have stubs be automatically generated in the REST Docs output directory. The following UML diagram shows the REST Docs flow:

"API Producer"->"API Producer": Add Spring Cloud Contract (SCC) \nStub Runner dependency
"API Producer"->"API Producer": Set up stub jar assembly
"API Producer"->"API Producer": Write and set up REST Docs tests
"API Producer"->"Build": Run build
"Build"->"REST Docs": Generate API \ndocumentation
"REST Docs"->"SCC": Generate stubs from the \nREST Docs tests
"REST Docs"->"SCC": Generate contracts from the \nREST Docs tests
"Build"->"Build": Assemble stubs jar with \nstubs and contracts
"Build"->"Nexus / Artifactory": Upload contracts \nand stubs and the project arifact
"Build"->"API Producer": Build successful
"API Consumer"->"API Consumer": Add SCC Stub Runner \ndependency
"API Consumer"->"API Consumer": Write a SCC Stub Runner \nbased contract test
"SCC Stub Runner"->"Nexus / Artifactory": Test asks for [API Producer] stubs
"Nexus / Artifactory"->"SCC Stub Runner": Fetch the [API Producer] stubs
"SCC Stub Runner"->"SCC Stub Runner": Run in memory\n HTTP server stubs
"API Consumer"->"SCC Stub Runner": Send a request \nto the HTTP server stub
"SCC Stub Runner"->"API Consumer": Communication is correct

以下示例使用了 MockMvc

The following example uses 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 stub。它匹配对 /resource 路径的所有 GET 请求。使用 WebTestClient(用于测试 Spring WebFlux 应用程序)的相同示例如下:

This test generates a WireMock stub at target/snippets/stubs/resource.json. It matches all GET requests to the /resource path. The same example with WebTestClient (used for testing Spring WebFlux applications) would be as follows:

@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 方法的请求匹配器和所有标题(hostcontent-length 除外)创建 stub。为了更精确地匹配请求(例如,匹配 POST 或 PUT 的主体),我们需要显式创建一个请求匹配器。这样做具有两个影响:

Without any additional configuration, these tests create a stub with a request matcher for the HTTP method and all headers except host and content-length. To match the request more precisely (for example, to match the body of a POST or PUT), we need to explicitly create a request matcher. Doing so has two effects:

  • Creating a stub that matches only in the way you specify.

  • Asserting that the request in the test case also matches the same conditions.

此功能的主要入口是 WireMockRestDocs.verify(),它可以用作 document() 便利方法的替代品,如下面的示例所示:

The main entry point for this feature is WireMockRestDocs.verify(), which can be used as a substitute for the document() convenience method, as the following example shows:

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 documentation 可为您提供帮助,帮助您培养速度。此测试的 WebTestClient 版本具有类似的 verify() 静态帮助器,您可将此帮助器插入相同的位置。

The preceding contract specifies that any valid POST with an id field receives the response defined in this test. You can chain together calls to .jsonPath() to add additional matchers. If JSON Path is unfamiliar, the JayWay documentation can help you get up to speed. The WebTestClient version of this test has a similar verify() static helper that you insert in the same place.

除了 jsonPathcontentType 便利方法外,你还可以使用 WireMock API 来验证请求是否与创建的 stub 匹配,如下面的示例所示:

Instead of the jsonPath and contentType convenience methods, you can also use the WireMock APIs to verify that the request matches the created stub, as the following example shows:

@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 路径匹配标题、查询参数和请求体。你可以使用这些功能通过范围更广的参数创建 stub。前一个示例生成的 stub 类似于以下示例:

The WireMock API is rich. You can match headers, query parameters, and the request body by regex as well as by JSON path. You can use these features to create stubs with a wider range of parameters. The preceding example generates a stub resembling the following example:

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() 方法创建请求匹配器,但不能同时使用两种方法。

You can use either the wiremock() method or the jsonPath() and contentType() methods to create request matchers, but you cannot use both approaches.

在消费者端,您可以让 resource.json 在本部分中更早地生成在类路径中可用(例如通过 Publishing Stubs as JARs)。随后,您可以生成使用 WireMock 的存根,方式有多种,其中包括使用 @AutoConfigureWireMock(stubs="classpath:resource.json"),如前面部分所述。

On the consumer side, you can make the resource.json generated earlier in this section available on the classpath (by Publishing Stubs as JARs, for example). After that, you can create a stub that uses WireMock in a number of different ways, including by using @AutoConfigureWireMock(stubs="classpath:resource.json"), as described earlier in this document.

Generating Contracts with REST Docs

您还可以在组合 Spring Cloud WireMock 时使用 Spring RESTDocs 生成 Spring Cloud Contract DSL 文件和文档。如果您这样做,既可以获得合约,也可以获得存根。

You can also generate Spring Cloud Contract DSL files and documentation with Spring REST Docs. If you do so in combination with Spring Cloud WireMock, you get both the contracts and the stubs.

为什么要使用这个特性呢?社区中一些人员曾询问有关这样的情况的问题,他们希望转向基于 DSL 的合约定义,但他们已经有很多 Spring MVC 测试了。使用这个特性可以让您生成合约文件,您可以在稍后对其进行修改并将其移动到文件夹(在您的配置中定义)中,以便插件能找到它们。

Why would you want to use this feature? Some people in the community asked questions about a situation in which they would like to move to DSL-based contract definition, but they already have a lot of Spring MVC tests. Using this feature lets you generate the contract files that you can later modify and move to folders (defined in your configuration) so that the plugin finds them.

您可能想知道为什么此功能存在于 WireMock 模块中。此功能之所以存在,是因为生成契约和存根很有意义。

You might wonder why this functionality is in the WireMock module. The functionality is there because it makes sense to generate both the contracts and the stubs.

请考虑以下测试:

Consider the following test:

Unresolved directive in rest-docs.adoc - include::{wiremock_tests}/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippetTests.java[]

上一个测试创建了上一部分提到的存根,生成了合约和文档文件。

The preceding test creates the stub presented in the previous section, generating both the contract and a documentation file.

合约被称作 index.groovy,它可能与以下示例类似:

The contract is called index.groovy and might resemble the following example:

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

The generated document (formatted in Asciidoc in this case) contains a formatted contract. The location of this file would be index/dsl-contract.adoc.

Specifying the priority attribute

SpringCloudContractRestDocs.dslContract() 方法接受一个可选的映射参数,它允许您在模板中指定附加属性。

The method SpringCloudContractRestDocs.dslContract() takes an optional Map parameter that allows you to specify additional attributes in the template.

其中一个属性是 priority 字段,你可以指定如下:

One of these attributes is the priority field that you may specify as follows:

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

Overriding the DSL contract template

默认情况下,合约的输出基于一个名为 default-dsl-contract-only.snippet 的文件。

By default, the output of the contract is based on a file named default-dsl-contract-only.snippet.

您可以通过覆盖 getTemplate() 方法来提供自定义的模板文件,如下所示:

You may provide a custom template file instead by overriding the getTemplate() method as follows:

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

因此,显示此行的上述示例

so the example above showing this line

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

应该更改为:

should be changed to:

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

通过查找类路径上的资源来解析模板。按照顺序检查以下位置:

Templates are resolved by looking for resources on the classpath. The following locations are checked in order:

  • 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

Therefore in the example above you should place a file named custom-dsl-template.snippet in src/test/resources/org/springframework/restdocs/templates/custom-dsl-template.snippet