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 流程:
"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
:
@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 应用程序)的相同示例如下:
@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
除外)创建 stub。为了更精确地匹配请求(例如,匹配 POST 或 PUT 的主体),我们需要显式创建一个请求匹配器。这样做具有两个影响:
-
创建一个仅按你指定方式进行匹配的存根。
-
断言测试用例中的请求也匹配相同的条件。
此功能的主要入口是 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
documentation 可为您提供帮助,帮助您培养速度。此测试的 WebTestClient 版本具有类似的 verify()
静态帮助器,您可将此帮助器插入相同的位置。
除了 jsonPath
和 contentType
便利方法外,你还可以使用 WireMock API 来验证请求是否与创建的 stub 匹配,如下面的示例所示:
@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 类似于以下示例:
{
"request" : {
"url" : "/resource",
"method" : "POST",
"bodyPatterns" : [ {
"matchesJsonPath" : "$.id"
}]
},
"response" : {
"status" : 200,
"body" : "Hello World",
"headers" : {
"X-Application-Context" : "application:-1",
"Content-Type" : "text/plain"
}
}
}
可以使用 |
在消费者端,您可以让 resource.json
在本部分中更早地生成在类路径中可用(例如通过 Publishing Stubs as JARs)。随后,您可以生成使用 WireMock 的存根,方式有多种,其中包括使用 @AutoConfigureWireMock(stubs="classpath:resource.json")
,如前面部分所述。
Generating Contracts with REST Docs
您还可以在组合 Spring Cloud WireMock 时使用 Spring RESTDocs 生成 Spring Cloud Contract DSL 文件和文档。如果您这样做,既可以获得合约,也可以获得存根。
为什么要使用这个特性呢?社区中一些人员曾询问有关这样的情况的问题,他们希望转向基于 DSL 的合约定义,但他们已经有很多 Spring MVC 测试了。使用这个特性可以让您生成合约文件,您可以在稍后对其进行修改并将其移动到文件夹(在您的配置中定义)中,以便插件能找到它们。
您可能想知道为什么此功能存在于 WireMock 模块中。此功能之所以存在,是因为生成契约和存根很有意义。 |
请考虑以下测试:
Unresolved directive in rest-docs.adoc - include::{wiremock_tests}/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippetTests.java[]
上一个测试创建了上一部分提到的存根,生成了合约和文档文件。
合约被称作 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
。
Specifying the priority attribute
SpringCloudContractRestDocs.dslContract()
方法接受一个可选的映射参数,它允许您在模板中指定附加属性。
其中一个属性是 priority 字段,你可以指定如下:
SpringCloudContractRestDocs.dslContract(Map.of("priority", 1))
Overriding the DSL contract template
默认情况下,合约的输出基于一个名为 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
中