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 方法的请求匹配器和所有标题(hostcontent-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() 静态帮助器,您可将此帮助器插入相同的位置。 除了 jsonPathcontentType 便利方法外,你还可以使用 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 类似于以下示例:

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 在本部分中更早地生成在类路径中可用(例如通过 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 模块中。此功能之所以存在,是因为生成契约和存根很有意义。

请考虑以下测试:

link:{wiremock_tests}/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippetTests.java[role=include]

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

合约被称作 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