通过将手写文档与使用 Spring MVC Test 或 WebTestClient 生成的自动生成的代码段相结合,记录 RESTful 服务。

介绍

Spring REST Docs 的目标是帮助您为 RESTful 服务生成准确且易读的文档。

编写高质量的文档很困难。减轻这种困难的一种方法是使用适合这项工作的工具。为此,Spring REST Docs 默认使用 Asciidoctor。Asciidoctor 处理纯文本并生成 HTML,样式和布局以满足您的需求。如果您愿意,您还可以将 Spring REST Docs 配置为使用 Markdown。

Spring REST Docs 使用由使用 Spring MVC 的 测试框架、Spring WebFlux 的 WebTestClientREST Assured 5 编写的测试生成的代码段。这种测试驱动的方法有助于保证服务文档的准确性。如果代码段不正确,则生成它的测试将失败。

记录 RESTful 服务主要是描述其资源。每个资源描述的两个关键部分是它消耗的 HTTP 请求的详细信息以及它生成的 HTTP 响应。Spring REST Docs 允许您使用这些资源以及 HTTP 请求和响应,从而使您的文档免受服务实现内部细节的影响。这种分离有助于您记录服务的 API 而不是其实现。它还使您能够在无需修改文档的情况下改进实现。

入门

本节介绍如何开始使用 Spring REST Docs。

示例应用程序

如果您想直接开始,可以使用许多示例应用程序

要求

Spring REST Docs 有以下最低要求:

  • Java 17

  • Spring Framework 6

此外,spring-restdocs-restassured 模块需要 REST Assured 5.2。

构建配置

使用 Spring REST Docs 的第一步是配置项目的构建。 Spring HATEOASSpring Data REST 示例分别包含 build.gradlepom.xml,您可以将其用作参考。配置的关键部分在以下列表中描述:

Maven
<dependency> (1)
	<groupId>org.springframework.restdocs</groupId>
	<artifactId>spring-restdocs-mockmvc</artifactId>
	<version>{project-version}</version>
	<scope>test</scope>
</dependency>

<build>
	<plugins>
		<plugin> (2)
			<groupId>org.asciidoctor</groupId>
			<artifactId>asciidoctor-maven-plugin</artifactId>
			<version>2.2.1</version>
			<executions>
				<execution>
					<id>generate-docs</id>
					<phase>prepare-package</phase> (3)
					<goals>
						<goal>process-asciidoc</goal>
					</goals>
					<configuration>
						<backend>html</backend>
						<doctype>book</doctype>
					</configuration>
				</execution>
			</executions>
			<dependencies>
				<dependency> (4)
					<groupId>org.springframework.restdocs</groupId>
					<artifactId>spring-restdocs-asciidoctor</artifactId>
					<version>{project-version}</version>
				</dependency>
			</dependencies>
		</plugin>
	</plugins>
</build>
1 test 范围内添加对 spring-restdocs-mockmvc 的依赖。如果您想使用 WebTestClient 或 REST Assured 而不是 MockMvc,请分别添加对 spring-restdocs-webtestclientspring-restdocs-restassured 的依赖。
2 添加 Asciidoctor 插件。
3 使用 prepare-package 允许将文档包含在包中
4 spring-restdocs-asciidoctor 添加为 Asciidoctor 插件的依赖项。这将自动配置 .adoc 文件中使用的 snippets 属性,使其指向 target/generated-snippets。它还允许您使用 operation 块宏。
Gradle
plugins { (1)
	id "org.asciidoctor.jvm.convert" version "3.3.2"
}

configurations {
	asciidoctorExt (2)
}

dependencies {
	asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' (3)
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' (4)
}

ext { (5)
	snippetsDir = file('build/generated-snippets')
}

test { (6)
	outputs.dir snippetsDir
}

asciidoctor { (7)
	inputs.dir snippetsDir (8)
	configurations 'asciidoctorExt' (9)
	dependsOn test (10)
}
1 应用 Asciidoctor 插件。
2 声明 asciidoctorExt 配置,用于扩展 Asciidoctor 的依赖项。
3 asciidoctorExt 配置中添加对 spring-restdocs-asciidoctor 的依赖。这将自动配置 snippets 属性,以便在您的 .adoc 文件中使用,指向 build/generated-snippets。它还允许您使用 operation 块宏。
4 testImplementation 配置中添加对 spring-restdocs-mockmvc 的依赖。如果您想使用 WebTestClient 或 REST Assured 而不是 MockMvc,请分别添加对 spring-restdocs-webtestclientspring-restdocs-restassured 的依赖。
5 配置 snippetsDir 属性,该属性定义生成片段的输出位置。
6 使 Gradle 知道运行 test 任务将输出写入 snippetsDir。这对于 增量构建 是必需的。
7 配置 asciidoctor 任务。
8 使 Gradle 知道运行任务将从 snippetsDir 读取输入。这对于 增量构建 是必需的。
9 配置使用 asciidoctorExt 配置扩展。
10 使任务依赖于 test 任务,以便在创建文档之前运行测试。

打包文档

你可能希望将生成的文档打包到项目的 jar 文件中——例如,让它作为静态内容提供 由 Spring Boot 提供。为此,请配置项目的构建,以便:

  1. 在构建 jar 之前生成文档

  2. 生成的文档包含在 jar 中

以下列表展示了如何在 Maven 和 Gradle 中执行此操作:

Maven
<plugin> (1)
	<groupId>org.asciidoctor</groupId>
	<artifactId>asciidoctor-maven-plugin</artifactId>
	<!-- … -->
</plugin>
<plugin> (2)
	<artifactId>maven-resources-plugin</artifactId>
	<version>2.7</version>
	<executions>
		<execution>
			<id>copy-resources</id>
			<phase>prepare-package</phase>
			<goals>
				<goal>copy-resources</goal>
			</goals>
			<configuration> (3)
				<outputDirectory>
					${project.build.outputDirectory}/static/docs
				</outputDirectory>
				<resources>
					<resource>
						<directory>
							${project.build.directory}/generated-docs
						</directory>
					</resource>
				</resources>
			</configuration>
		</execution>
	</executions>
</plugin>
1 Asciidoctor 插件的现有声明。
2 资源插件必须在 Asciidoctor 插件之后声明,因为它们绑定到同一个阶段(prepare-package),并且资源插件必须在 Asciidoctor 插件之后运行,以确保在复制之前生成文档。
3 将生成的文档复制到构建输出的 static/docs 目录中,从那里它将被包含在 jar 文件中。
Gradle
bootJar {
	dependsOn asciidoctor (1)
	from ("${asciidoctor.outputDir}/html5") { (2)
		into 'static/docs'
	}
}
1 确保在构建 jar 之前已生成文档。
2 将生成的文档复制到 jar 的 static/docs 目录中。

生成文档片段

Spring REST Docs 使用 Spring MVC 的 测试框架、Spring WebFlux 的 WebTestClientREST Assured 来向您正在编写的文档的服务发出请求。然后,它会为请求和结果响应生成文档片段。

设置测试

确切的测试设置方法取决于您使用的测试框架。Spring REST Docs 为 JUnit 5 和 JUnit 4 提供了 一流的支持。建议使用 JUnit 5。其他框架,如 TestNG,也支持,尽管需要稍微多一些设置。

设置你的 JUnit 5 测试

使用 JUnit 5 时,生成文档代码段的第一步是将 RestDocumentationExtension 应用到你的测试类。以下示例展示了如何做到这一点:

@ExtendWith(RestDocumentationExtension.class)
public class JUnit5ExampleTests {

当测试典型的 Spring 应用程序时,你还应该应用 SpringExtension

@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class JUnit5ExampleTests {

RestDocumentationExtension 会根据你的项目构建工具自动配置一个输出目录:

构建工具 输出目录

Maven

target/generated-snippets

Gradle

build/generated-snippets

如果你使用的是 JUnit 5.1,你可以通过将扩展作为字段注册到你的测试类中,并在创建时提供输出目录来覆盖默认值。以下示例展示了如何做到这一点:

public class JUnit5ExampleTests {

	@RegisterExtension
	final RestDocumentationExtension restDocumentation = new RestDocumentationExtension ("custom");

}

接下来,你必须提供一个 @BeforeEach 方法来配置 MockMvc 或 WebTestClient,或 REST Assured。以下列表展示了如何做到这一点:

MockMvc
private MockMvc mockMvc;

@BeforeEach
void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
	this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
		.apply(documentationConfiguration(restDocumentation)) (1)
		.build();
}
1 MockMvc 实例是使用 MockMvcRestDocumentationConfigurer 配置的。你可以从 org.springframework.restdocs.mockmvc.MockMvcRestDocumentation 上的静态 documentationConfiguration() 方法获取此类的实例。
WebTestClient
private WebTestClient webTestClient;

@BeforeEach
void setUp(ApplicationContext applicationContext, RestDocumentationContextProvider restDocumentation) {
	this.webTestClient = WebTestClient.bindToApplicationContext(applicationContext)
		.configureClient()
		.filter(documentationConfiguration(restDocumentation)) (1)
		.build();
}
1 通过添加一个 `WebTestClientRestDocumentationConfigurer` 作为 `ExchangeFilterFunction` 来配置 `WebTestClient` 实例。你可以从 `org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation` 的静态 `documentationConfiguration()` 方法获得此类的实例。
REST Assured
private RequestSpecification spec;

@BeforeEach
void setUp(RestDocumentationContextProvider restDocumentation) {
	this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation)) (1)
		.build();
}
1 通过添加一个 `RestAssuredRestDocumentationConfigurer` 作为 `Filter` 来配置 REST Assured。你可以从 `org.springframework.restdocs.restassured` 包中 `RestAssuredRestDocumentation` 的静态 `documentationConfiguration()` 方法获得此类的实例。

配置器应用合理的默认值,并提供用于自定义配置的 API。有关更多信息,请参阅 配置部分

设置 JUnit 4 测试

当使用 JUnit 4 时,生成文档片段的第一步是声明一个用 JUnit @Rule 注解的 public JUnitRestDocumentation 字段。以下示例展示了如何执行此操作:

@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();

默认情况下,JUnitRestDocumentation 规则会根据项目的构建工具自动配置输出目录:

构建工具 输出目录

Maven

target/generated-snippets

Gradle

build/generated-snippets

您可以通过在创建 JUnitRestDocumentation 实例时提供输出目录来覆盖默认值。以下示例展示了如何执行此操作:

@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("custom");

接下来,您必须提供一个 @Before 方法来配置 MockMvc 或 WebTestClient 或 REST Assured。以下示例展示了如何执行此操作:

MockMvc
private MockMvc mockMvc;

@Autowired
private WebApplicationContext context;

@Before
public void setUp() {
	this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
		.apply(documentationConfiguration(this.restDocumentation)) (1)
		.build();
}
1 MockMvc 实例使用 MockMvcRestDocumentationConfigurer 进行配置。您可以从 org.springframework.restdocs.mockmvc.MockMvcRestDocumentation 上的静态 documentationConfiguration() 方法获取此类的实例。
WebTestClient
private WebTestClient webTestClient;

@Autowired
private ApplicationContext context;

@Before
public void setUp() {
	this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
		.configureClient()
		.filter(documentationConfiguration(this.restDocumentation)) (1)
		.build();
}
1 WebTestClient 实例通过添加 WebTestclientRestDocumentationConfigurer 作为 ExchangeFilterFunction 进行配置。您可以从 org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation 的静态 documentationConfiguration() 方法获取此类的实例。
REST Assured
private RequestSpecification spec;

@Before
public void setUp() {
	this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)) (1)
		.build();
}
1 REST Assured 通过添加 RestAssuredRestDocumentationConfigurer 作为 Filter 进行配置。您可以从 org.springframework.restdocs.restassured 包中 RestAssuredRestDocumentation 的静态 documentationConfiguration() 方法获取此类的实例。

配置器应用合理的默认值,并提供用于自定义配置的 API。有关更多信息,请参阅配置部分

在没有 JUnit 的情况下设置测试

未使用 JUnit 时的配置与使用时的配置基本相似。本节描述了关键区别。TestNG 示例也说明了这种方法。

第一个区别是,你应该使用 ManualRestDocumentation 来代替 JUnitRestDocumentation。此外,你不需要 @Rule 注解。以下示例展示了如何使用 ManualRestDocumentation

private ManualRestDocumentation restDocumentation = new ManualRestDocumentation();

其次,你必须在每个测试之前调用 ManualRestDocumentation.beforeTest(Class, String)。你可以将其作为配置 MockMvc、WebTestClient 或 REST Assured 的方法的一部分。以下示例展示了如何做到这一点:

MockMvc
private MockMvc mockMvc;

@Autowired
private WebApplicationContext context;

@BeforeMethod
public void setUp(Method method) {
	this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
		.apply(documentationConfiguration(this.restDocumentation))
		.build();
	this.restDocumentation.beforeTest(getClass(), method.getName());
}
WebTestClient
private WebTestClient webTestClient;

@Autowired
private ApplicationContext context;

@BeforeMethod
public void setUp(Method method) {
	this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
		.configureClient()
		.filter(documentationConfiguration(this.restDocumentation)) (1)
		.build();
	this.restDocumentation.beforeTest(getClass(), method.getName());
}
REST Assured
private RequestSpecification spec;

@BeforeMethod
public void setUp(Method method) {
	this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)).build();
	this.restDocumentation.beforeTest(getClass(), method.getName());
}

最后,你必须在每个测试之后调用 ManualRestDocumentation.afterTest。以下示例展示了如何使用 TestNG 做到这一点:

@AfterMethod
public void tearDown() {
	this.restDocumentation.afterTest();
}

调用RESTful服务

现在您已经配置了测试框架,您可以使用它来调用RESTful服务并记录请求和响应。以下示例展示了如何执行此操作:

MockMvc
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) (1)
	.andExpect(status().isOk()) (2)
	.andDo(document("index")); (3)
1 调用服务的根 (/) 并指示需要 application/json 响应。
2 断言服务产生了预期的响应。
3 记录对服务的调用,将片段写入名为 index 的目录(位于配置的输出目录下)。片段由 RestDocumentationResultHandler 写入。您可以从 org.springframework.restdocs.mockmvc.MockMvcRestDocumentation 上的静态 document 方法获取此类的实例。
WebTestClient
this.webTestClient.get()
	.uri("/")
	.accept(MediaType.APPLICATION_JSON) (1)
	.exchange()
	.expectStatus()
	.isOk() (2)
	.expectBody()
	.consumeWith(document("index")); (3)
1 调用服务的根 (/) 并指示需要 application/json 响应。
2 断言服务产生了预期的响应。
3 记录对服务的调用,将片段写入名为 index 的目录(位于配置的输出目录下)。片段由 ExchangeResultConsumer 写入。您可以从 org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation 上的静态 document 方法获取此类消费者。
REST Assured
RestAssured.given(this.spec) (1)
	.accept("application/json") (2)
	.filter(document("index")) (3)
	.when()
	.get("/") (4)
	.then()
	.assertThat()
	.statusCode(is(200)); (5)
1 应用在 @Before 方法中初始化的规范。
2 表明需要 application/json 响应。
3 记录对服务的调用,将代码段写入名为 index 的目录(位于配置的输出目录下)。代码段由 RestDocumentationFilter 编写。您可以从 org.springframework.restdocs.restassured 包中 RestAssuredRestDocumentation 的静态 document 方法获取此类的实例。
4 调用服务的根目录 (/)。
5 断言服务产生预期的响应。

默认情况下,会写入六个代码段:

  • <输出目录>/index/curl-request.adoc

  • <输出目录>/index/http-request.adoc

  • <输出目录>/index/http-response.adoc

  • <输出目录>/index/httpie-request.adoc

  • <输出目录>/index/request-body.adoc

  • <输出目录>/index/response-body.adoc

有关这些以及 Spring REST Docs 可以生成的其他代码段的更多信息,请参阅 记录您的 API

使用代码段

在使用生成的代码段之前,必须创建一个 .adoc 源文件。你可以随意命名该文件,只要它具有 .adoc 后缀。生成的HTML文件具有相同的名称,但带有 .html 后缀。源文件和生成的HTML文件的默认位置取决于你使用的是Maven还是Gradle:

构建工具 源文件 生成的文件

Maven

src/main/asciidoc/*.adoc

target/generated-docs/*.html

Gradle

src/docs/asciidoc/*.adoc

build/asciidoc/html5/*.html

然后,你可以使用包含宏将生成的代码段包含在本节前面描述的手动创建的Asciidoc文件中。你可以使用spring-restdocs-asciidoctor构建配置中配置自动设置的snippets属性来引用代码段输出目录。以下示例展示了如何执行此操作:

include::{snippets}/index/curl-request.adoc[]

记录您的API

本节详细介绍了如何使用 Spring REST Docs 来记录您的 API。

超媒体

Spring REST Docs 提供了对基于 超媒体 的 API 中链接的文档支持。以下示例展示了如何使用它:

MockMvc
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("index", links((1)
			linkWithRel("alpha").description("指向 Alpha 资源的链接"), (2)
			linkWithRel("bravo").description("指向 Bravo 资源的链接")))); (3)
1 配置 Spring REST Docs 以生成描述响应链接的片段。使用 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上的静态 links 方法。
2 期望 relalpha 的链接。使用 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上的静态 linkWithRel 方法。
3 期望 relbravo 的链接。
WebTestClient
this.webTestClient.get()
	.uri("/")
	.accept(MediaType.APPLICATION_JSON)
	.exchange()
	.expectStatus()
	.isOk()
	.expectBody()
	.consumeWith(document("index", links((1)
			linkWithRel("alpha").description("指向 Alpha 资源的链接"), (2)
			linkWithRel("bravo").description("指向 Bravo 资源的链接")))); (3)
1 配置 Spring REST Docs 以生成描述响应链接的片段。使用 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上的静态 links 方法。
2 期望 relalpha 的链接。使用 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上的静态 linkWithRel 方法。
3 期望 relbravo 的链接。
REST Assured
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("index", links((1)
			linkWithRel("alpha").description("指向 Alpha 资源的链接"), (2)
			linkWithRel("bravo").description("指向 Bravo 资源的链接")))) (3)
	.get("/")
	.then()
	.assertThat()
	.statusCode(is(200));
1 配置 Spring REST docs 以生成描述响应链接的代码段。使用 `org.springframework.restdocs.hypermedia.HypermediaDocumentation` 上的静态 `links` 方法。
2 期望一个 `rel` 为 `alpha` 的链接。使用 `org.springframework.restdocs.hypermedia.HypermediaDocumentation` 上的静态 `linkWithRel` 方法。
3 期望一个 `rel` 为 `bravo` 的链接。

结果是一个名为 `links.adoc` 的代码段,其中包含一个描述资源链接的表格。

如果响应中的链接具有 `title`,则可以从其描述符中省略描述,并使用 `title`。如果省略描述并且链接没有 `title`,则会发生失败。

在记录链接时,如果在响应中发现未记录的链接,则测试将失败。同样,如果在响应中找不到已记录的链接并且该链接未标记为可选,则测试也会失败。

如果您不想记录链接,可以将其标记为忽略。这样做可以防止它出现在生成的代码段中,同时避免上述失败。

您还可以以宽松模式记录链接,在这种模式下,任何未记录的链接都不会导致测试失败。为此,请使用 `org.springframework.restdocs.hypermedia.HypermediaDocumentation` 上的 `relaxedLinks` 方法。在记录特定场景时,这可能很有用,在这种情况下,您只想关注链接的子集。

默认情况下支持两种链接格式:

  • Atom: 链接预计在一个名为 links 的数组中。当响应的内容类型与 application/json 兼容时,默认使用此格式。

  • HAL: 链接预计在一个名为 _links 的映射中。当响应的内容类型与 application/hal+json 兼容时,默认使用此格式。

如果您使用 Atom 或 HAL 格式的链接,但内容类型不同,您可以向 links 提供内置的 LinkExtractor 实现之一。以下示例展示了如何执行此操作:

MockMvc
.andDo(document("index", links(halLinks(), (1)
		linkWithRel("alpha").description("指向 alpha 资源的链接"),
		linkWithRel("bravo").description("指向 bravo 资源的链接"))));
1 指示链接采用 HAL 格式。使用 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上的静态 halLinks 方法。
WebTestClient
.consumeWith(document("index", links(halLinks(), (1)
		linkWithRel("alpha").description("指向 alpha 资源的链接"),
		linkWithRel("bravo").description("指向 bravo 资源的链接"))));
1 指示链接采用 HAL 格式。使用 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上的静态 halLinks 方法。
REST Assured
.filter(document("index", links(halLinks(), (1)
		linkWithRel("alpha").description("指向 alpha 资源的链接"),
		linkWithRel("bravo").description("指向 bravo 资源的链接"))))
1 指示链接采用 HAL 格式。使用 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上的静态 halLinks 方法。

如果您的 API 以 Atom 或 HAL 以外的格式表示其链接,您可以提供您自己的 LinkExtractor 接口的实现,以从响应中提取链接。

与其记录每个响应中常见的链接,例如使用 HAL 时的 selfcuries,您可能希望在概述部分中记录它们一次,然后在 API 文档的其余部分中忽略它们。为此,您可以基于对重用代码段的支持将链接描述符添加到预先配置为忽略某些链接的代码段。以下示例演示了如何执行此操作:

public static LinksSnippet links(LinkDescriptor... descriptors) {
	return HypermediaDocumentation.links(linkWithRel("self").ignored().optional(), linkWithRel("curies").ignored())
		.and(descriptors);
}

请求和响应有效负载

除了前面描述的超媒体特定支持之外,还提供了对请求和响应有效负载的一般文档支持。

默认情况下,Spring REST Docs 会自动为请求正文和响应正文生成代码段。这些代码段分别命名为 request-body.adocresponse-body.adoc

请求和响应字段

为了提供更详细的请求或响应有效负载文档,支持记录有效负载的字段。

考虑以下有效负载:

{
	"contact": {
		"name": "Jane Doe",
		"email": "[email protected]"
	}
}

你可以按照以下方式记录前面的例子中的字段:

MockMvc
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("index", responseFields((1)
			fieldWithPath("contact.email").description("用户的电子邮件地址"), (2)
			fieldWithPath("contact.name").description("用户的姓名")))); (3)
1 配置Spring REST docs生成一个描述响应有效负载中字段的片段。要记录请求,可以使用requestFields。两者都是org.springframework.restdocs.payload.PayloadDocumentation的静态方法。
2 预期路径为contact.email的字段。使用org.springframework.restdocs.payload.PayloadDocumentation上的静态fieldWithPath方法。
3 预期路径为contact.name的字段。
WebTestClient
this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("user",
		responseFields((1)
			fieldWithPath("contact.email").description("用户的电子邮件地址"), (2)
			fieldWithPath("contact.name").description("用户的姓名")))); (3)
1 配置Spring REST docs生成一个描述响应有效负载中字段的片段。要记录请求,可以使用requestFields。两者都是org.springframework.restdocs.payload.PayloadDocumentation的静态方法。
2 预期路径为contact.email的字段。使用org.springframework.restdocs.payload.PayloadDocumentation上的静态fieldWithPath方法。
3 预期路径为contact.name的字段。
REST Assured
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("user", responseFields((1)
			fieldWithPath("contact.name").description("用户的姓名"), (2)
			fieldWithPath("contact.email").description("用户的邮箱地址")))) (3)
	.when()
	.get("/user/5")
	.then()
	.assertThat()
	.statusCode(is(200));
1 配置 Spring REST docs 以生成描述响应负载中字段的代码段。要记录请求,可以使用 requestFields。两者都是 org.springframework.restdocs.payload.PayloadDocumentation 上的静态方法。
2 期望路径为 contact.email 的字段。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 fieldWithPath 方法。
3 期望路径为 contact.name 的字段。

结果是一个包含描述字段表格的代码段。对于请求,此代码段名为 request-fields.adoc。对于响应,此代码段名为 response-fields.adoc

在记录字段时,如果在负载中找到未记录的字段,则测试将失败。类似地,如果在负载中未找到已记录的字段,并且该字段未标记为可选,则测试也将失败。

如果您不想为所有字段提供详细的文档,则可以记录负载的整个子部分。以下示例展示了如何执行此操作:

MockMvc
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("index", responseFields((1)
			subsectionWithPath("contact").description("用户的联系方式")))); (1)
1 记录路径为 contact 的子部分。contact.emailcontact.name 现在也被视为已记录。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 subsectionWithPath 方法。
WebTestClient
this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("user",
		responseFields(
			subsectionWithPath("contact").description("用户的联系方式")))); (1)
1 使用路径 contact 记录子部分。 contact.emailcontact.name 现在也被视为已记录。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 subsectionWithPath 方法。
REST Assured
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("user",
			responseFields(subsectionWithPath("contact").description("用户的联系方式")))) (1)
	.when()
	.get("/user/5")
	.then()
	.assertThat()
	.statusCode(is(200));
1 使用路径 contact 记录子部分。 contact.emailcontact.name 现在也被视为已记录。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 subsectionWithPath 方法。

subsectionWithPath 可用于提供有效负载特定部分的高级概述。然后,您可以为子部分生成单独的、更详细的文档。请参阅 记录请求或响应有效负载的子部分

如果您根本不想记录某个字段或子部分,可以将其标记为忽略。这可以防止它出现在生成的代码段中,同时避免前面描述的失败。

您还可以以宽松模式记录字段,其中任何未记录的字段都不会导致测试失败。为此,请使用 org.springframework.restdocs.payload.PayloadDocumentation 上的 relaxedRequestFieldsrelaxedResponseFields 方法。当记录您只想关注有效负载子集的特定场景时,这会很有用。

默认情况下,Spring REST Docs 假设您正在记录的有效负载是 JSON。如果您想记录 XML 有效负载,则请求或响应的内容类型必须与 application/xml 兼容。
JSON有效负载中的字段

本节介绍如何使用JSON有效负载中的字段。

JSON 字段路径

JSON 字段路径使用点表示法或括号表示法。点表示法使用“.”分隔路径中的每个键(例如,a.b)。括号表示法将每个键括在方括号和单引号中(例如,['a']['b'])。在任何一种情况下,[] 都用于标识数组。点表示法更简洁,但使用括号表示法可以在键名中使用“.”(例如,['a.b'])。两种不同的表示法可以在同一路径中使用(例如,a['b'])。

考虑以下 JSON 有效负载:

{
	"a":{
		"b":[
			{
				"c":"one"
			},
			{
				"c":"two"
			},
			{
				"d":"three"
			}
		],
		"e.dot" : "four"
	}
}

在前面的 JSON 有效负载中,存在以下路径:

路径

a

包含 b 的对象

a.b

包含三个对象的数组

['a']['b']

包含三个对象的数组

a['b']

包含三个对象的数组

['a'].b

包含三个对象的数组

a.b[]

包含三个对象的数组

a.b[].c

包含字符串 onetwo 的数组

a.b[].d

字符串 three

a['e.dot']

字符串 four

['a']['e.dot']

字符串 four

您还可以记录一个使用数组作为根的有效负载。路径 [] 指的是整个数组。然后,您可以使用括号或点符号来标识数组条目中的字段。例如,[].id 对应于以下数组中每个对象的 id 字段:

[
	{
		"id":1
	},
	{
		"id":2
	}
]

您可以使用 * 作为通配符来匹配具有不同名称的字段。例如,users.*.role 可用于记录以下 JSON 中每个用户的角色:

{
	"users":{
		"ab12cd34":{
			"role": "Administrator"
		},
		"12ab34cd":{
			"role": "Guest"
		}
	}
}
JSON 字段类型

当一个字段被记录时,Spring REST Docs 会通过检查有效负载来尝试确定其类型。支持七种不同的类型:

类型 描述

array

字段每次出现的都是数组。

boolean

字段每次出现的都是布尔值(truefalse)。

object

字段每次出现的都是对象。

number

字段每次出现的都是数字。

null

字段每次出现的都是 null

string

字段每次出现的都是字符串。

varies

该字段在有效负载中多次出现,具有各种不同的类型。

您还可以使用 FieldDescriptor 上的 type(Object) 方法显式设置类型。提供的 ObjecttoString 方法的结果将用于文档中。通常,使用 JsonFieldType 枚举的值之一。以下示例展示了如何做到这一点:

MockMvc
.andDo(document("index", responseFields(fieldWithPath("contact.email").type(JsonFieldType.STRING) (1)
	.description("用户的电子邮件地址"))));
1 将字段的类型设置为 String
WebTestClient
.consumeWith(document("user",
	responseFields(
		fieldWithPath("contact.email")
			.type(JsonFieldType.STRING) (1)
			.description("用户的电子邮件地址"))));
1 将字段类型设置为 String
REST Assured
.filter(document("user", responseFields(fieldWithPath("contact.email").type(JsonFieldType.STRING) (1)
	.description("用户的电子邮件地址"))))
1 将字段类型设置为 String
XML有效载荷

本节描述如何使用XML有效载荷。

XML字段路径

XML字段路径使用XPath描述。/ 用于进入子节点。

XML字段类型

当记录XML有效载荷时,您必须通过在FieldDescriptor上使用type(Object)方法来提供字段的类型。提供的类型的toString方法的结果用于文档中。

重用字段描述符

除了对重用代码片段的常规支持外,请求和响应代码片段允许使用路径前缀配置其他描述符。这允许为请求或响应有效负载的重复部分创建一次描述符,然后重复使用。

考虑一个返回书籍的端点:

{
	"title": "傲慢与偏见",
	"author": "简·奥斯汀"
}

titleauthor 的路径分别为 titleauthor

现在考虑一个返回书籍数组的端点:

[{
	"title": "傲慢与偏见",
	"author": "简·奥斯汀"
},
{
	"title": "杀死一只知更鸟",
	"author": "哈珀·李"
}]

titleauthor 的路径分别为 [].title[].author。单本书和书籍数组之间的唯一区别是字段的路径现在有一个 []. 前缀。

您可以按如下方式创建描述书籍的描述符:

FieldDescriptor[] book = new FieldDescriptor[] { fieldWithPath("title").description("书名"),
		fieldWithPath("author").description("作者") };

然后您可以使用它们来记录单本书,如下所示:

MockMvc
this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("book", responseFields(book))); (1)
1 使用现有描述符记录 titleauthor
WebTestClient
this.webTestClient.get().uri("/books/1").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("book",
		responseFields(book))); (1)
1 使用现有描述符记录 titleauthor
REST Assured
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("book", responseFields(book))) (1)
	.when()
	.get("/books/1")
	.then()
	.assertThat()
	.statusCode(is(200));
1 使用现有描述符记录 标题作者

您也可以使用描述符来记录书籍数组,如下所示:

MockMvc
this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("book", responseFields(fieldWithPath("[]").description("一个书籍数组")) (1)
		.andWithPrefix("[].", book))); (2)
1 记录数组。
2 使用带有 []. 前缀的现有描述符记录 [].title[].author
WebTestClient
this.webTestClient.get().uri("/books").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("books",
		responseFields(
			fieldWithPath("[]")
				.description("一个书籍数组")) (1)
				.andWithPrefix("[].", book))); (2)
1 记录数组。
2 使用带有 []. 前缀的现有描述符记录 [].title[].author
REST Assured
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("books", responseFields(fieldWithPath("[]").description("一个书籍数组")) (1)
		.andWithPrefix("[].", book))) (2)
	.when()
	.get("/books")
	.then()
	.assertThat()
	.statusCode(is(200));
1 记录数组。
2 使用带有 []. 前缀的现有描述符记录 [].title[].author

记录请求或响应有效负载的子部分

如果有效负载很大或结构复杂,记录有效负载的各个部分会很有用。REST Docs 允许您通过提取有效负载的子部分然后对其进行记录来实现这一点。

记录请求或响应正文的子部分

考虑以下 JSON 响应正文:

{
	"weather": {
		"wind": {
			"speed": 15.3,
			"direction": 287.0
		},
		"temperature": {
			"high": 21.2,
			"low": 14.8
		}
	}
}

您可以生成一个片段来记录 temperature 对象,如下所示:

MockMvc
this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("location", responseBody(beneathPath("weather.temperature")))); (1)
1 生成包含响应正文子部分的片段。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 responseBodybeneathPath 方法。要生成请求正文的片段,您可以使用 requestBody 代替 responseBody
WebTestClient
this.webTestClient.get().uri("/locations/1").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("temperature",
		responseBody(beneathPath("weather.temperature")))); (1)
1 生成包含响应正文子部分的片段。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 responseBodybeneathPath 方法。要生成请求正文的片段,您可以使用 requestBody 代替 responseBody
REST Assured
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("location", responseBody(beneathPath("weather.temperature")))) (1)
	.when()
	.get("/locations/1")
	.then()
	.assertThat()
	.statusCode(is(200));
1 生成包含响应正文子部分的片段。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 responseBodybeneathPath 方法。要生成请求正文的片段,您可以使用 requestBody 代替 responseBody

结果是一个包含以下内容的片段:

{
	"temperature": {
		"high": 21.2,
		"low": 14.8
	}
}

为了使代码片段的名称具有区分度,其中包含了子部分的标识符。默认情况下,此标识符为 beneath-${path}。例如,前面的代码会生成一个名为 response-body-beneath-weather.temperature.adoc 的代码片段。你可以使用 withSubsectionId(String) 方法来自定义标识符,如下所示:

responseBody(beneathPath("weather.temperature").withSubsectionId("temp"));

结果是一个名为 request-body-temp.adoc 的代码片段。

记录请求或响应子部分的字段

除了记录请求或响应主体的子部分之外,您还可以记录特定子部分中的字段。您可以生成一个代码片段来记录 temperature 对象(highlow)的字段,如下所示:

MockMvc
this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("location", responseFields(beneathPath("weather.temperature"), (1)
			fieldWithPath("high").description("摄氏度预测最高温度"), (2)
			fieldWithPath("low").description("摄氏度预测最低温度"))));
1 生成一个代码片段,描述响应有效负载中路径 weather.temperature 下方子部分中的字段。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 beneathPath 方法。
2 记录 highlow 字段。
WebTestClient
this.webTestClient.get().uri("/locations/1").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("temperature",
		responseFields(beneathPath("weather.temperature"), (1)
			fieldWithPath("high").description("摄氏度预测最高温度"), (2)
			fieldWithPath("low").description("摄氏度预测最低温度"))));
1 生成一个代码片段,描述响应有效负载中路径 weather.temperature 下方子部分中的字段。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 beneathPath 方法。
2 记录 highlow 字段。
REST Assured
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("location", responseFields(beneathPath("weather.temperature"), (1)
			fieldWithPath("high").description("摄氏度预测最高温度"), (2)
			fieldWithPath("low").description("摄氏度预测最低温度"))))
	.when()
	.get("/locations/1")
	.then()
	.assertThat()
	.statusCode(is(200));
1 生成一个代码片段,描述响应有效负载中路径 weather.temperature 下的子部分中的字段。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 beneathPath 方法。
2 记录 highlow 字段。

结果是一个代码片段,其中包含一个表格,描述了 weather.temperaturehighlow 字段。为了使代码片段的名称清晰,包含了子部分的标识符。默认情况下,此标识符为 beneath-${path}。例如,前面的代码会生成一个名为 response-fields-beneath-weather.temperature.adoc 的代码片段。

查询参数

您可以使用 queryParameters 记录请求的查询参数。以下示例展示了如何执行此操作:

MockMvc
this.mockMvc.perform(get("/users?page=2&per_page=100")) (1)
	.andExpect(status().isOk())
	.andDo(document("users", queryParameters((2)
			parameterWithName("page").description("要检索的页码"), (3)
			parameterWithName("per_page").description("每页条目数") (4)
	)));
1 执行一个 GET 请求,在查询字符串中包含两个参数,pageper_page
2 配置 Spring REST Docs 以生成一个描述请求查询参数的片段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 queryParameters 方法。
3 记录 page 参数。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 parameterWithName 方法。
4 记录 per_page 参数。
WebTestClient
this.webTestClient.get().uri("/users?page=2&per_page=100") (1)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("users", queryParameters((2)
			parameterWithName("page").description("要检索的页码"), (3)
			parameterWithName("per_page").description("每页条目数") (4)
	)));
1 执行一个 GET 请求,在查询字符串中包含两个参数,pageper_page
2 配置 Spring REST Docs 以生成一个描述请求查询参数的片段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 queryParameters 方法。
3 记录 page 参数。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 parameterWithName 方法。
4 记录 per_page 参数。
REST Assured
RestAssured.given(this.spec)
	.filter(document("users", queryParameters((1)
			parameterWithName("page").description("要检索的页面"), (2)
			parameterWithName("per_page").description("每页条目数")))) (3)
	.when()
	.get("/users?page=2&per_page=100") (4)
	.then()
	.assertThat()
	.statusCode(is(200));
1 配置 Spring REST Docs 以生成描述请求查询参数的代码段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 queryParameters 方法。
2 记录 page 参数。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 parameterWithName 方法。
3 记录 per_page 参数。
4 使用查询字符串中的两个参数 pageper_page 执行 GET 请求。

在记录查询参数时,如果请求的查询字符串中使用了未记录的查询参数,则测试将失败。同样,如果在请求的查询字符串中找不到已记录的查询参数,并且该参数未标记为可选,则测试也会失败。

如果您不想记录某个查询参数,可以将其标记为忽略。这将阻止它出现在生成的代码段中,同时避免上述失败。

您还可以以宽松模式记录查询参数,在这种模式下,任何未记录的参数都不会导致测试失败。为此,请使用 org.springframework.restdocs.request.RequestDocumentation 上的 relaxedQueryParameters 方法。当记录特定场景时,您只想关注查询参数的一个子集,这将非常有用。

表单参数

您可以使用 formParameters 来记录请求的表单参数。以下示例展示了如何进行此操作:

MockMvc
this.mockMvc.perform(post("/users").param("username", "Tester")) (1)
	.andExpect(status().isCreated())
	.andDo(document("create-user", formParameters((2)
			parameterWithName("username").description("用户名") (3)
	)));
1 执行一个包含单个表单参数 usernamePOST 请求。
2 配置 Spring REST Docs 以生成描述请求表单参数的片段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 formParameters 方法。
3 记录 username 参数。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 parameterWithName 方法。
WebTestClient
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("username", "Tester");
this.webTestClient.post().uri("/users").body(BodyInserters.fromFormData(formData)) (1)
		.exchange().expectStatus().isCreated().expectBody()
		.consumeWith(document("create-user", formParameters((2)
				parameterWithName("username").description("用户名") (3)
		)));
1 执行一个包含单个表单参数 usernamePOST 请求。
2 配置 Spring REST Docs 以生成描述请求表单参数的片段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 formParameters 方法。
3 记录 username 参数。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 parameterWithName 方法。
REST Assured
RestAssured.given(this.spec)
	.filter(document("create-user", formParameters((1)
			parameterWithName("username").description("用户名")))) (2)
	.formParam("username", "Tester")
	.when()
	.post("/users") (3)
	.then()
	.assertThat()
	.statusCode(is(200));
1 配置 Spring REST Docs 以生成描述请求表单参数的代码段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 formParameters 方法。
2 记录 username 参数。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 parameterWithName 方法。
3 执行带有单个表单参数 usernamePOST 请求。

在所有情况下,结果都是一个名为 form-parameters.adoc 的代码段,其中包含一个描述资源支持的表单参数的表格。

在记录表单参数时,如果请求体中使用了未记录的表单参数,则测试失败。类似地,如果在请求体中找不到已记录的表单参数,并且该表单参数尚未标记为可选,则测试也会失败。

如果您不想记录某个表单参数,可以将其标记为忽略。这可以防止它出现在生成的代码段中,同时避免上述失败。

您还可以以宽松模式记录表单参数,其中任何未记录的参数都不会导致测试失败。为此,请使用 org.springframework.restdocs.request.RequestDocumentation 上的 relaxedFormParameters 方法。在记录特定场景时,这很有用,在该场景中您只想关注表单参数的一个子集。

路径参数

您可以使用 pathParameters 来记录请求的路径参数。以下示例展示了如何做到这一点:

MockMvc
this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) (1)
	.andExpect(status().isOk())
	.andDo(document("locations", pathParameters((2)
			parameterWithName("latitude").description("位置的纬度"), (3)
			parameterWithName("longitude").description("位置的经度") (4)
	)));
1 执行带有两个路径参数(latitudelongitude)的 GET 请求。
2 配置 Spring REST Docs 以生成一个描述请求路径参数的片段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 pathParameters 方法。
3 记录名为 latitude 的参数。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 parameterWithName 方法。
4 记录名为 longitude 的参数。
WebTestClient
this.webTestClient.get().uri("/locations/{latitude}/{longitude}", 51.5072, 0.1275) (1)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("locations",
		pathParameters((2)
			parameterWithName("latitude").description("位置的纬度"), (3)
			parameterWithName("longitude").description("位置的经度")))); (4)
1 执行带有两个路径参数(latitudelongitude)的 GET 请求。
2 配置 Spring REST Docs 以生成一个描述请求路径参数的片段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 pathParameters 方法。
3 记录名为 latitude 的参数。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 parameterWithName 方法。
4 记录名为 longitude 的参数。
REST Assured
RestAssured.given(this.spec)
	.filter(document("locations", pathParameters((1)
			parameterWithName("latitude").description("位置的纬度"), (2)
			parameterWithName("longitude").description("位置的经度")))) (3)
	.when()
	.get("/locations/{latitude}/{longitude}", 51.5072, 0.1275) (4)
	.then()
	.assertThat()
	.statusCode(is(200));
1 配置 Spring REST Docs 以生成描述请求路径参数的片段。使用 `org.springframework.restdocs.request.RequestDocumentation` 上的静态 `pathParameters` 方法。
2 记录名为 `latitude` 的参数。使用 `org.springframework.restdocs.request.RequestDocumentation` 上的静态 `parameterWithName` 方法。
3 记录名为 `longitude` 的参数。
4 使用两个路径参数 `latitude` 和 `longitude` 执行 `GET` 请求。

结果是一个名为 `path-parameters.adoc` 的片段,其中包含一个表格,描述了资源支持的路径参数。

如果您使用 MockMvc,为了使路径参数可用于文档,您必须通过使用 `RestDocumentationRequestBuilders` 而不是 `MockMvcRequestBuilders` 上的方法来构建请求。

在记录路径参数时,如果请求中使用了未记录的路径参数,则测试将失败。类似地,如果请求中没有找到已记录的路径参数并且该路径参数没有被标记为可选,则测试也会失败。

您还可以以宽松模式记录路径参数,在这种模式下,任何未记录的参数都不会导致测试失败。为此,请使用 `org.springframework.restdocs.request.RequestDocumentation` 上的 `relaxedPathParameters` 方法。当记录特定场景时,这很有用,因为您只想关注路径参数的一个子集。

如果您不想记录某个路径参数,可以将其标记为忽略。这样做可以防止它出现在生成的片段中,同时避免前面描述的失败。

请求部分

您可以使用 requestParts 来记录多部分请求的各个部分。以下示例展示了如何执行此操作:

MockMvc
this.mockMvc.perform(multipart("/upload").file("file", "example".getBytes())) (1)
	.andExpect(status().isOk())
	.andDo(document("upload", requestParts((2)
			partWithName("file").description("要上传的文件")) (3)
	));
1 执行一个包含名为 file 的单个部分的 POST 请求。
2 配置 Spring REST Docs 以生成描述请求部分的代码段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 requestParts 方法。
3 记录名为 file 的部分。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 partWithName 方法。
WebTestClient
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
multipartData.add("file", "example".getBytes());
this.webTestClient.post().uri("/upload").body(BodyInserters.fromMultipartData(multipartData)) (1)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("upload", requestParts((2)
		partWithName("file").description("要上传的文件")) (3)
));
1 执行一个包含名为 file 的单个部分的 POST 请求。
2 配置 Spring REST Docs 以生成描述请求部分的代码段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 requestParts 方法。
3 记录名为 file 的部分。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 partWithName 方法。
REST Assured
RestAssured.given(this.spec)
	.filter(document("users", requestParts((1)
			partWithName("file").description("要上传的文件")))) (2)
	.multiPart("file", "example") (3)
	.when()
	.post("/upload") (4)
	.then()
	.statusCode(is(200));
1 将 Spring REST Docs 配置为生成描述请求部分的片段。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 requestParts 方法。
2 记录名为 file 的部分。使用 org.springframework.restdocs.request.RequestDocumentation 上的静态 partWithName 方法。
3 使用名为 file 的部分配置请求。
4 执行对 /uploadPOST 请求。

结果是一个名为 request-parts.adoc 的片段,其中包含一个表格,描述了资源支持的请求部分。

记录请求部分时,如果请求中使用了未记录的部分,则测试将失败。同样,如果在请求中找不到已记录的部分并且该部分未标记为可选,则测试也会失败。

您还可以在宽松模式下记录请求部分,在这种模式下,任何未记录的部分都不会导致测试失败。为此,请使用 org.springframework.restdocs.request.RequestDocumentation 上的 relaxedRequestParts 方法。这在记录特定场景时非常有用,您只想关注请求部分的子集。

如果您不想记录请求部分,可以将其标记为忽略。这可以防止它出现在生成的片段中,同时避免前面描述的失败。

请求部分有效负载

您可以像记录请求的有效负载一样记录请求部分的有效负载,并支持记录请求部分的正文及其字段。

记录请求部分的正文

您可以按如下方式生成包含请求部分正文的代码段:

MockMvc
MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", "<<png data>>".getBytes());
MockMultipartFile metadata = new MockMultipartFile("metadata", "", "application/json",
		"{ \"version\": \"1.0\"}".getBytes());

this.mockMvc.perform(multipart("/images").file(image).file(metadata).accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("image-upload", requestPartBody("metadata"))); (1)
1 配置 Spring REST docs 以生成包含名为 metadata 的请求部分正文的代码段。使用 PayloadDocumentation 上的静态 requestPartBody 方法。
WebTestClient
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
Resource imageResource = new ByteArrayResource("<<png data>>".getBytes()) {

	@Override
	public String getFilename() {
		return "image.png";
	}

};
multipartData.add("image", imageResource);
multipartData.add("metadata", Collections.singletonMap("version",  "1.0"));

this.webTestClient.post().uri("/images").body(BodyInserters.fromMultipartData(multipartData))
	.accept(MediaType.APPLICATION_JSON).exchange()
	.expectStatus().isOk().expectBody()
	.consumeWith(document("image-upload",
			requestPartBody("metadata"))); (1)
1 配置 Spring REST docs 以生成包含名为 metadata 的请求部分正文的代码段。使用 PayloadDocumentation 上的静态 requestPartBody 方法。
REST Assured
Map<String, String> metadata = new HashMap<>();
metadata.put("version", "1.0");
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("image-upload", requestPartBody("metadata"))) (1)
	.when()
	.multiPart("image", new File("image.png"), "image/png")
	.multiPart("metadata", metadata)
	.post("images")
	.then()
	.assertThat()
	.statusCode(is(200));
1 配置 Spring REST docs 以生成包含名为 metadata 的请求部分正文的代码段。使用 PayloadDocumentation 上的静态 requestPartBody 方法。

结果是一个名为 request-part-${part-name}-body.adoc 的代码段,其中包含该部分的正文。例如,记录名为 metadata 的部分会生成一个名为 request-part-metadata-body.adoc 的代码段。

记录请求部分的字段

您可以像记录请求或响应的字段一样,记录请求部分的字段,如下所示:

MockMvc
MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", "<<png data>>".getBytes());
MockMultipartFile metadata = new MockMultipartFile("metadata", "", "application/json",
		"{ \"version\": \"1.0\"}".getBytes());

this.mockMvc.perform(multipart("/images").file(image).file(metadata).accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("image-upload", requestPartFields("metadata", (1)
			fieldWithPath("version").description("图片的版本")))); (2)
1 配置 Spring REST docs 以生成一个描述名为 metadata 的请求部分的有效负载中的字段的代码段。使用 PayloadDocumentation 上的静态 requestPartFields 方法。
2 期望有一个路径为 version 的字段。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 fieldWithPath 方法。
WebTestClient
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
Resource imageResource = new ByteArrayResource("<<png data>>".getBytes()) {

	@Override
	public String getFilename() {
		return "image.png";
	}

};
multipartData.add("image", imageResource);
multipartData.add("metadata", Collections.singletonMap("version",  "1.0"));
this.webTestClient.post().uri("/images").body(BodyInserters.fromMultipartData(multipartData))
	.accept(MediaType.APPLICATION_JSON).exchange()
	.expectStatus().isOk().expectBody()
	.consumeWith(document("image-upload",
		requestPartFields("metadata", (1)
			fieldWithPath("version").description("图片的版本")))); (2)
1 配置 Spring REST docs 以生成一个描述名为 metadata 的请求部分的有效负载中的字段的代码段。使用 PayloadDocumentation 上的静态 requestPartFields 方法。
2 期望有一个路径为 version 的字段。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 fieldWithPath 方法。
REST Assured
Map<String, String> metadata = new HashMap<>();
metadata.put("version", "1.0");
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("image-upload", requestPartFields("metadata", (1)
			fieldWithPath("version").description("图像的版本")))) (2)
	.when()
	.multiPart("image", new File("image.png"), "image/png")
	.multiPart("metadata", metadata)
	.post("images")
	.then()
	.assertThat()
	.statusCode(is(200));
1 配置 Spring REST docs 以生成一个描述名为 metadata 的请求部分有效负载中字段的代码段。使用 PayloadDocumentation 上的静态 requestPartFields 方法。
2 预期路径为 version 的字段。使用 org.springframework.restdocs.payload.PayloadDocumentation 上的静态 fieldWithPath 方法。

结果是一个包含描述部分字段的表格的代码段。此代码段名为 request-part-${part-name}-fields.adoc。例如,记录名为 metadata 的部分会生成一个名为 request-part-metadata-fields.adoc 的代码段。

在记录字段时,如果在部分的有效负载中发现未记录的字段,则测试失败。同样,如果在部分的有效负载中找不到记录的字段并且该字段尚未标记为可选,则测试也会失败。对于具有层次结构的有效负载,记录一个字段就足以将其所有子代都视为已记录。

如果您不想记录某个字段,可以将其标记为忽略。这样做可以防止它出现在生成的代码段中,同时避免上述失败。

您还可以以宽松模式记录字段,在这种模式下,任何未记录的字段都不会导致测试失败。为此,请使用 org.springframework.restdocs.payload.PayloadDocumentation 上的 relaxedRequestPartFields 方法。当记录您只想关注部分有效负载的特定场景时,这会很有用。

有关描述字段、记录使用 XML 的有效负载等的更多信息,请参阅有关记录请求和响应有效负载的部分

HTTP 头

您可以分别使用 requestHeadersresponseHeaders 来记录请求或响应中的头。以下示例展示了如何执行此操作:

MockMvc
this.mockMvc.perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) (1)
	.andExpect(status().isOk())
	.andDo(document("headers", requestHeaders((2)
			headerWithName("Authorization").description("基本身份验证凭证")), (3)
			responseHeaders((4)
					headerWithName("X-RateLimit-Limit")
						.description("每个周期允许的总请求数"),
					headerWithName("X-RateLimit-Remaining")
						.description("当前周期内允许的剩余请求数"),
					headerWithName("X-RateLimit-Reset")
						.description("速率限制周期重置的时间"))));
1 执行带有使用基本身份验证的 Authorization 头的 GET 请求。
2 配置 Spring REST Docs 以生成描述请求头的片段。使用 org.springframework.restdocs.headers.HeaderDocumentation 上的静态 requestHeaders 方法。
3 记录 Authorization 头。使用 org.springframework.restdocs.headers.HeaderDocumentation 上的静态 headerWithName 方法。
4 生成描述响应头的片段。使用 org.springframework.restdocs.headers.HeaderDocumentation 上的静态 responseHeaders 方法。
WebTestClient
this.webTestClient
	.get().uri("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=") (1)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("headers",
		requestHeaders((2)
			headerWithName("Authorization").description("基本身份验证凭证")), (3)
		responseHeaders((4)
			headerWithName("X-RateLimit-Limit")
				.description("每个周期允许的总请求数"),
			headerWithName("X-RateLimit-Remaining")
				.description("当前周期内允许的剩余请求数"),
			headerWithName("X-RateLimit-Reset")
				.description("速率限制周期重置的时间"))));
1 使用带有基本身份验证的 Authorization 标头执行 GET 请求。
2 配置 Spring REST Docs 以生成描述请求标头的代码段。使用 org.springframework.restdocs.headers.HeaderDocumentation 上的静态 requestHeaders 方法。
3 记录 Authorization 标头。使用 org.springframework.restdocs.headers.HeaderDocumentation 上的静态 headerWithName 方法。
4 生成描述响应标头的代码段。使用 org.springframework.restdocs.headers.HeaderDocumentation 上的静态 responseHeaders 方法。
REST Assured
RestAssured.given(this.spec)
	.filter(document("headers", requestHeaders((1)
			headerWithName("Authorization").description("基本身份验证凭证")), (2)
			responseHeaders((3)
					headerWithName("X-RateLimit-Limit")
						.description("每周期允许的总请求数"),
					headerWithName("X-RateLimit-Remaining")
						.description("当前周期内允许的剩余请求"),
					headerWithName("X-RateLimit-Reset")
						.description("速率限制周期重置的时间"))))
	.header("Authorization", "Basic dXNlcjpzZWNyZXQ=") (4)
	.when()
	.get("/people")
	.then()
	.assertThat()
	.statusCode(is(200));
1 配置 Spring REST Docs 以生成描述请求标头的代码段。使用 org.springframework.restdocs.headers.HeaderDocumentation 上的静态 requestHeaders 方法。
2 记录 Authorization 标头。使用 `org.springframework.restdocs.headers.HeaderDocumentation 上的静态 headerWithName 方法。
3 生成描述响应标头的代码段。使用 org.springframework.restdocs.headers.HeaderDocumentation 上的静态 responseHeaders 方法。
4 使用带有基本身份验证的 Authorization 标头配置请求。

结果是一个名为 request-headers.adoc 的代码段和一个名为 response-headers.adoc 的代码段。每个代码段都包含一个描述标头的表格。

在记录 HTTP 标头时,如果在请求或响应中找不到记录的标头,则测试将失败。

HTTP Cookies

您可以使用 requestCookiesresponseCookies 分别记录请求和响应中的 cookie。以下示例展示了如何操作:

MockMvc
this.mockMvc.perform(get("/").cookie(new Cookie("JSESSIONID", "ACBCDFD0FF93D5BB"))) (1)
	.andExpect(status().isOk())
	.andDo(document("cookies", requestCookies((2)
			cookieWithName("JSESSIONID").description("会话令牌")), (3)
			responseCookies((4)
					cookieWithName("JSESSIONID").description("更新后的会话令牌"),
					cookieWithName("logged_in")
						.description("如果用户当前已登录,则设置为 true"))));
1 使用 JSESSIONID cookie 发出 GET 请求。
2 配置 Spring REST Docs 以生成描述请求 cookie 的代码段。使用 org.springframework.restdocs.cookies.CookieDocumentation 上的静态 requestCookies 方法。
3 记录 JSESSIONID cookie。使用 org.springframework.restdocs.cookies.CookieDocumentation 上的静态 cookieWithName 方法。
4 生成描述响应 cookie 的代码段。使用 org.springframework.restdocs.cookies.CookieDocumentation 上的静态 responseCookies 方法。
WebTestClient
this.webTestClient.get()
	.uri("/people")
	.cookie("JSESSIONID", "ACBCDFD0FF93D5BB=") (1)
	.exchange()
	.expectStatus()
	.isOk()
	.expectBody()
	.consumeWith(document("cookies", requestCookies((2)
			cookieWithName("JSESSIONID").description("会话令牌")), (3)
			responseCookies((4)
					cookieWithName("JSESSIONID").description("更新后的会话令牌"),
					cookieWithName("logged_in").description("用户已登录"))));
1 使用 JSESSIONID cookie 进行 GET 请求。
2 配置 Spring REST Docs 生成描述请求 cookies 的代码片段。使用 org.springframework.restdocs.cookies.CookieDocumentation 上的静态 requestCookies 方法。
3 记录 JSESSIONID cookie。使用 org.springframework.restdocs.cookies.CookieDocumentation 上的静态 cookieWithName 方法。
4 生成描述响应 cookies 的代码片段。使用 org.springframework.restdocs.cookies.CookieDocumentation 上的静态 responseCookies 方法。
REST Assured
RestAssured.given(this.spec)
	.filter(document("cookies", requestCookies((1)
			cookieWithName("JSESSIONID").description("已保存的会话令牌")), (2)
			responseCookies((3)
					cookieWithName("logged_in").description("如果用户已登录"),
					cookieWithName("JSESSIONID").description("已更新的会话令牌"))))
	.cookie("JSESSIONID", "ACBCDFD0FF93D5BB") (4)
	.when()
	.get("/people")
	.then()
	.assertThat()
	.statusCode(is(200));
1 配置 Spring REST Docs 生成描述请求 cookies 的代码片段。使用 org.springframework.restdocs.cookies.CookieDocumentation 上的静态 requestCookies 方法。
2 记录 JSESSIONID cookie。使用 org.springframework.restdocs.cookies.CookieDocumentation 上的静态 cookieWithName 方法。
3 生成描述响应 cookies 的代码片段。使用 org.springframework.restdocs.cookies.CookieDocumentation 上的静态 responseCookies 方法。
4 使用请求发送 JSESSIONID cookie。

结果是一个名为 request-cookies.adoc 的代码片段和一个名为 response-cookies.adoc 的代码片段。每个代码片段都包含一个描述 cookies 的表格。

在记录 HTTP cookies 时,如果在请求或响应中发现未记录的 cookie,则测试失败。同样,如果未找到记录的 cookie 并且该 cookie 未标记为可选,则测试也会失败。您也可以在宽松模式下记录 cookies,在这种模式下,任何未记录的 cookies 都不会导致测试失败。为此,请使用 org.springframework.restdocs.cookies.CookieDocumentation 上的 relaxedRequestCookiesrelaxedResponseCookies 方法。这在记录特定场景时非常有用,在这些场景中,您只想关注 cookies 的一个子集。如果您不想记录某个 cookie,可以将其标记为忽略。这样做可以防止它出现在生成的代码片段中,同时避免前面描述的失败。

重复使用代码片段

通常,正在记录的API有一些功能在几个资源之间是通用的。为了避免在记录这些资源时重复,您可以重复使用配置了公共元素的Snippet

首先,创建描述公共元素的Snippet。以下示例显示了如何执行此操作:

protected final LinksSnippet pagingLinks = links(
		linkWithRel("first").optional().description("结果的第一页"),
		linkWithRel("last").optional().description("结果的最后一页"),
		linkWithRel("next").optional().description("结果的下一页"),
		linkWithRel("prev").optional().description("结果的上一页"));

其次,使用此代码片段并添加资源特定的其他描述符。以下示例显示了如何执行此操作:

MockMvc
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("example", this.pagingLinks.and((1)
			linkWithRel("alpha").description("链接到 alpha 资源"),
			linkWithRel("bravo").description("链接到 bravo 资源"))));
1 重复使用pagingLinks Snippet,调用and添加特定于正在记录的资源的描述符。
WebTestClient
this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange()
	.expectStatus().isOk().expectBody()
	.consumeWith(document("example", this.pagingLinks.and((1)
		linkWithRel("alpha").description("链接到 alpha 资源"),
		linkWithRel("bravo").description("链接到 bravo 资源"))));
1 重复使用pagingLinks Snippet,调用and添加特定于正在记录的资源的描述符。
REST Assured
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("example", this.pagingLinks.and((1)
			linkWithRel("alpha").description("链接到 alpha 资源"),
			linkWithRel("bravo").description("链接到 bravo 资源"))))
	.get("/")
	.then()
	.assertThat()
	.statusCode(is(200));
1 重复使用pagingLinks Snippet,调用and添加特定于正在记录的资源的描述符。

示例结果是,带有 rel 值为 firstlastnextpreviousalphabravo 的链接都已记录在文档中。

记录约束

Spring REST Docs 提供了许多类来帮助您记录约束。您可以使用 ConstraintDescriptions 的实例来访问类约束的描述。以下示例展示了如何做到这一点:

public void example() {
	ConstraintDescriptions userConstraints = new ConstraintDescriptions(UserInput.class); (1)
	List<String> descriptions = userConstraints.descriptionsForProperty("name"); (2)
}

static class UserInput {

	@NotNull
	@Size(min = 1)
	String name;

	@NotNull
	@Size(min = 8)
	String password;

}
1 UserInput 类创建 ConstraintDescriptions 的实例。
2 获取 name 属性约束的描述。此列表包含两个描述:一个用于 NotNull 约束,另一个用于 Size 约束。

Spring HATEOAS 示例中的 ApiDocumentation 类展示了此功能的实际应用。

查找约束

默认情况下,约束是通过使用 Bean Validation Validator 找到的。目前,仅支持属性约束。您可以通过使用自定义 ValidatorConstraintResolver 实例创建 ConstraintDescriptions 来定制所使用的 Validator。要完全控制约束解析,您可以使用自己的 ConstraintResolver 实现。

描述约束

为所有 Bean Validation 3.0 的约束提供了默认描述:

  • AssertFalse

  • AssertTrue

  • DecimalMax

  • DecimalMin

  • Digits

  • Email

  • Future

  • FutureOrPresent

  • Max

  • Min

  • Negative

  • NegativeOrZero

  • NotBlank

  • NotEmpty

  • NotNull

  • Null

  • Past

  • PastOrPresent

  • Pattern

  • Positive

  • PositiveOrZero

  • Size

还为以下来自 Hibernate Validator 的约束提供了默认描述:

  • CodePointLength

  • CreditCardNumber

  • Currency

  • EAN

  • Email

  • Length

  • LuhnCheck

  • Mod10Check

  • Mod11Check

  • NotBlank

  • NotEmpty

  • Currency

  • Range

  • SafeHtml

  • URL

要覆盖默认描述或提供新的描述,可以创建一个名为 org.springframework.restdocs.constraints.ConstraintDescriptions 的资源包。基于 Spring HATEOAS 的示例包含 这样一个资源包的示例

资源包中的每个键都是约束的全限定名加上 .description。例如,标准 @NotNull 约束的键是 jakarta.validation.constraints.NotNull.description

可以在其描述中使用引用约束属性的属性占位符。例如,@Min 约束的默认描述 Must be at least ${value} 引用了约束的 value 属性。

要更好地控制约束描述解析,可以使用自定义 ResourceBundleConstraintDescriptionResolver 创建 ConstraintDescriptions。要完全控制,可以使用自定义 ConstraintDescriptionResolver 实现创建 ConstraintDescriptions

在生成的代码段中使用约束描述

一旦你获得了约束的描述,你就可以在生成的代码段中随意使用它们。例如,你可能想将约束描述包含在字段描述的一部分。或者,你可以将约束作为请求字段代码段中的额外信息包含在内。Spring HATEOAS-based 示例中的 ApiDocumentation 类说明了后一种方法。

默认代码片段

当您记录请求和响应时,会自动生成许多代码片段。

代码片段 描述

curl-request.adoc

包含相当于正在记录的 MockMvc 调用的 curl 命令。

httpie-request.adoc

包含相当于正在记录的 MockMvc 调用的 HTTPie 命令。

http-request.adoc

包含相当于正在记录的 MockMvc 调用的 HTTP 请求。

http-response.adoc

包含返回的 HTTP 响应。

request-body.adoc

包含发送的请求体。

response-body.adoc

包含返回的响应体。

您可以配置默认生成的代码片段。有关更多信息,请参阅 配置部分

使用参数化输出目录

你可以参数化document使用的输出目录。支持以下参数:

参数 描述

{methodName}

测试方法的未修改名称。

{method-name}

使用 kebab-case 格式化的测试方法名称。

{method_name}

使用 snake_case 格式化的测试方法名称。

{ClassName}

测试类的未修改简单名称。

{class-name}

使用 kebab-case 格式化的测试类简单名称。

{class_name}

使用 snake_case 格式化的测试类简单名称。

{step}

当前测试中对服务的调用次数。

例如,在测试类 GettingStartedDocumentation 上名为 creatingANote 的测试方法中使用 document("{class-name}/{method-name}") 会将代码段写入名为 getting-started-documentation/creating-a-note 的目录中。

参数化输出目录与 @Before 方法结合使用特别有用。它允许在设置方法中配置一次文档,然后在类中的每个测试中重复使用。以下示例展示了如何做到这一点:

MockMvc
@Before
public void setUp() {
	this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
		.apply(documentationConfiguration(this.restDocumentation))
		.alwaysDo(document("{method-name}/{step}/"))
		.build();
}
REST Assured
@Before
public void setUp() {
	this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation))
		.addFilter(document("{method-name}/{step}"))
		.build();
}
WebTestClient
@Before
public void setUp() {
	this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
		.configureClient()
		.filter(documentationConfiguration(this.restDocumentation))
		.entityExchangeResultConsumer(document("{method-name}/{step}"))
		.build();
}

配置完成后,对您正在测试的服务的每个调用都会生成默认代码段,而无需任何进一步的配置。查看每个示例应用程序中的 GettingStartedDocumentation 类,了解此功能的实际应用。

自定义输出

本节介绍如何自定义 Spring REST Docs 的输出。

自定义生成的代码段

Spring REST Docs 使用 Mustache 模板生成代码段。Spring REST Docs 提供了 默认模板 用于生成各种代码段。若要自定义代码段的内容,您可以提供自己的模板。

模板从类路径中的 org.springframework.restdocs.templates 子包加载。子包的名称由所使用的模板格式的 ID 决定。默认模板格式 Asciidoctor 的 ID 为 asciidoctor,因此代码段从 org.springframework.restdocs.templates.asciidoctor 加载。每个模板都以它生成的代码段命名。例如,要覆盖 curl-request.adoc 代码段的模板,请在 src/test/resources/org/springframework/restdocs/templates/asciidoctor 中创建一个名为 curl-request.snippet 的模板。

包含额外信息

有两种方法可以提供额外的信息,以便包含在生成的代码片段中:

  • 在描述符上使用 attributes 方法向其添加一个或多个属性。

  • 在调用 curlRequesthttpRequesthttpResponse 等时传入一些属性。这些属性与整个代码片段关联。

在模板渲染过程中,任何额外的属性都可用。结合自定义代码片段模板,这使得在生成的代码片段中包含额外信息成为可能。

一个具体的例子是在记录请求字段时添加一个约束列和一个标题。第一步是为您记录的每个字段提供一个 constraints 属性,并提供一个 title 属性。以下示例展示了如何做到这一点:

MockMvc
.andDo(document("create-user", requestFields(attributes(key("title").value("用于创建用户的字段")), (1)
		fieldWithPath("name").description("用户名")
			.attributes(key("constraints").value("不能为空。不能为空字符串")), (2)
		fieldWithPath("email").description("用户电子邮件地址")
			.attributes(key("constraints").value("必须是有效的电子邮件地址"))))); (3)
1 为请求字段代码片段配置 title 属性。
2 name 字段设置 constraints 属性。
3 email 字段设置 constraints 属性。
WebTestClient
.consumeWith(document("create-user",
	requestFields(
		attributes(key("title").value("用于创建用户的字段")), (1)
		fieldWithPath("name")
			.description("用户名")
			.attributes(key("constraints").value("不能为空。不能为空字符串")), (2)
		fieldWithPath("email")
			.description("用户电子邮件地址")
			.attributes(key("constraints").value("必须是有效的电子邮件地址"))))); (3)
1 为请求字段代码片段配置 title 属性。
2 name 字段设置 constraints 属性。
3 email 字段设置 constraints 属性。
REST Assured
.filter(document("create-user", requestFields(attributes(key("title").value("用户创建字段")), (1)
		fieldWithPath("name").description("用户名")
			.attributes(key("constraints").value("不能为空,也不能留空")), (2)
		fieldWithPath("email").description("用户邮箱地址")
			.attributes(key("constraints").value("必须是有效的邮箱地址"))))) (3)
1 配置请求字段片段的 title 属性。
2 name 字段设置 constraints 属性。
3 email 字段设置 constraints 属性。

第二步是提供一个名为 request-fields.snippet 的自定义模板,其中包含生成的片段表格中有关字段约束的信息,并添加一个标题。

.{{title}} (1)
|===
|路径|类型|描述|约束条件 (2)

{{#fields}}
|{{path}}
|{{type}}
|{{description}}
|{{constraints}} (3)

{{/fields}}
|===
1 为表格添加标题。
2 添加一个名为“约束条件”的新列。
3 将描述符的 constraints 属性包含在表格的每一行中。

自定义请求和响应

在某些情况下,您可能不希望按照发送请求或接收响应的原样记录它们。Spring REST Docs 提供了许多预处理器,可用于在记录请求或响应之前对其进行修改。

通过调用带有 OperationRequestPreprocessorOperationResponsePreprocessordocument 方法来配置预处理。您可以使用 Preprocessors 上的静态方法 preprocessRequestpreprocessResponse 获取实例。以下示例展示了如何执行此操作:

MockMvc
this.mockMvc.perform(get("/"))
	.andExpect(status().isOk())
	.andDo(document("index", preprocessRequest(modifyHeaders().remove("Foo")), (1)
			preprocessResponse(prettyPrint()))); (2)
1 应用请求预处理器以删除名为 Foo 的标头。
2 应用响应预处理器以美化其内容。
WebTestClient
this.webTestClient.get().uri("/").exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("index",
		preprocessRequest(modifyHeaders().remove("Foo")), (1)
		preprocessResponse(prettyPrint()))); (2)
1 应用请求预处理器以删除名为 Foo 的标头。
2 应用响应预处理器以美化其内容。
REST Assured
RestAssured.given(this.spec)
	.filter(document("index", preprocessRequest(modifyHeaders().remove("Foo")), (1)
			preprocessResponse(prettyPrint()))) (2)
	.when()
	.get("/")
	.then()
	.assertThat()
	.statusCode(is(200));
1 应用请求预处理器以删除名为 Foo 的标头。
2 应用响应预处理器以美化其内容。

或者,您可能希望对每个测试应用相同的预处理器。您可以使用 @Before 方法中的 RestDocumentationConfigurer API 来配置预处理器。例如,要从所有请求中删除 Foo 标头并美化所有响应,您可以执行以下操作之一(取决于您的测试环境):

MockMvc
private MockMvc mockMvc;

@Before
public void setup() {
	this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
		.apply(documentationConfiguration(this.restDocumentation).operationPreprocessors()
			.withRequestDefaults(modifyHeaders().remove("Foo")) (1)
			.withResponseDefaults(prettyPrint())) (2)
		.build();
}
1 应用一个请求预处理器,它将删除名为Foo的标头。
2 应用一个响应预处理器,它将美化其内容。
WebTestClient
private WebTestClient webTestClient;

@Before
public void setup() {
	this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
		.configureClient()
		.filter(documentationConfiguration(this.restDocumentation)
			.operationPreprocessors()
				.withRequestDefaults(modifyHeaders().remove("Foo")) (1)
				.withResponseDefaults(prettyPrint())) (2)
		.build();
}
1 应用一个请求预处理器,它将删除名为Foo的标头。
2 应用一个响应预处理器,它将美化其内容。
REST Assured
private RequestSpecification spec;

@Before
public void setup() {
	this.spec = new RequestSpecBuilder()
		.addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors()
			.withRequestDefaults(modifyHeaders().remove("Foo")) (1)
			.withResponseDefaults(prettyPrint())) (2)
		.build();
}
1 应用一个请求预处理器,它将删除名为Foo的标头。
2 应用一个响应预处理器,它将美化其内容。

然后,在每个测试中,您可以执行特定于该测试的任何配置。以下示例显示了如何执行此操作:

MockMvc
this.mockMvc.perform(get("/"))
	.andExpect(status().isOk())
	.andDo(document("index", links(linkWithRel("self").description("Canonical self link")))); 
WebTestClient
this.webTestClient.get().uri("/").exchange().expectStatus().isOk()
	.expectBody().consumeWith(document("index", 
		links(linkWithRel("self").description("Canonical self link")))); 
REST Assured
RestAssured.given(this.spec)
	.filter(document("index", links(linkWithRel("self").description("规范的自我链接"))))
	.when()
	.get("/")
	.then()
	.assertThat()
	.statusCode(is(200));

各种内置预处理器,包括上面说明的那些,都可以通过 Preprocessors 上的静态方法获得。有关更多详细信息,请参阅下方

预处理器

格式化输出

PreprocessorsprettyPrint 方法可以格式化请求或响应的内容,使其更易于阅读。

如果您正在记录基于超媒体的 API,您可能希望鼓励客户端使用链接而不是硬编码的 URI 来导航 API。一种方法是在文档中限制 URI 的使用。PreprocessorsmaskLinks 方法会将响应中任何链接的 href 替换为 …​。您也可以根据需要指定不同的替换文本。

修改标头

您可以使用 PreprocessorsmodifyHeaders 方法来添加、设置和删除请求或响应标头。

替换模式

PreprocessorsreplacePattern 方法提供了一种通用机制来替换请求或响应中的内容。任何与正则表达式匹配的内容都会被替换。

修改 URI

如果您使用 MockMvc 或未绑定到服务器的 WebTestClient,则应通过 更改配置 来定制 URI。

您可以使用 PreprocessorsmodifyUris 方法来修改请求或响应中的任何 URI。当使用 REST Assured 或绑定到服务器的 WebTestClient 时,这使您可以在测试服务的本地实例时自定义文档中显示的 URI。

编写您自己的预处理器

如果内置预处理器都不能满足您的需求,您可以通过实现 OperationPreprocessor 接口来编写自己的预处理器。然后,您可以像使用任何内置预处理器一样使用您的自定义预处理器。

如果您只想修改请求或响应的内容(正文),请考虑实现 ContentModifier 接口并将其与内置的 ContentModifyingOperationPreprocessor 一起使用。

配置

本节介绍如何配置 Spring REST Docs。

文档化 URI

本节介绍配置文档化的 URI。

MockMvc URI 定制

使用 MockMvc 时,Spring REST Docs 文档化的 URI 的默认配置如下:

设置 默认值

方案

http

主机

localhost

端口

8080

此配置由 MockMvcRestDocumentationConfigurer 应用。您可以使用其 API 更改一个或多个默认值以满足您的需求。以下示例演示了如何执行此操作:

this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
	.apply(documentationConfiguration(this.restDocumentation).uris()
		.withScheme("https")
		.withHost("example.com")
		.withPort(443))
	.build();
如果端口设置为已配置方案的默认端口(HTTP 端口 80 或 HTTPS 端口 443),则会从生成的代码段中的任何 URI 中省略该端口。
要配置请求的上下文路径,请使用 MockHttpServletRequestBuilder 上的 contextPath 方法。

REST Assured URI 定制

REST Assured 通过发出实际的 HTTP 请求来测试服务。因此,必须在对服务执行操作之后但在记录操作之前自定义 URI。为此目的提供了一个 特定于 REST Assured 的预处理器

WebTestClient URI 自定义

当使用 WebTestClient 时,Spring REST Docs 文档中 URI 的默认基础是 http://localhost:8080。你可以通过使用 baseUrl(String) 方法在 WebTestClient.Builder 来定制这个基础。以下示例展示了如何做到这一点:

@Before
public void setUp() {
	this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
		.configureClient()
		.baseUrl("https://api.example.com") (1)
		.filter(documentationConfiguration(this.restDocumentation))
		.build();
}
1 将文档化 URI 的基础配置为 https://api.example.com

代码段编码

默认代码段编码为 UTF-8。您可以使用 RestDocumentationConfigurer API 更改默认代码段编码。例如,以下示例使用 ISO-8859-1

MockMvc
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
	.apply(documentationConfiguration(this.restDocumentation).snippets().withEncoding("ISO-8859-1"))
	.build();
WebTestClient
this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient()
	.filter(documentationConfiguration(this.restDocumentation)
		.snippets().withEncoding("ISO-8859-1"))
	.build();
REST Assured
this.spec = new RequestSpecBuilder()
	.addFilter(documentationConfiguration(this.restDocumentation).snippets().withEncoding("ISO-8859-1"))
	.build();
当 Spring REST Docs 将请求或响应的内容转换为 String 时,如果 Content-Type 标头中指定的 charset 可用,则使用它。如果不存在,则使用 JVM 的默认 Charset。您可以使用 file.encoding 系统属性配置 JVM 的默认 Charset

代码段模板格式

默认代码段模板格式为 Asciidoctor。开箱即用也支持 Markdown。您可以使用 RestDocumentationConfigurer API 更改默认格式。以下示例展示了如何执行此操作:

MockMvc
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
	.apply(documentationConfiguration(this.restDocumentation).snippets()
		.withTemplateFormat(TemplateFormats.markdown()))
	.build();
WebTestClient
this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient()
	.filter(documentationConfiguration(this.restDocumentation)
		.snippets().withTemplateFormat(TemplateFormats.markdown()))
	.build();
REST Assured
this.spec = new RequestSpecBuilder()
	.addFilter(documentationConfiguration(this.restDocumentation).snippets()
		.withTemplateFormat(TemplateFormats.markdown()))
	.build();

默认代码片段

默认情况下会生成六个代码片段:

  • curl-request

  • http-request

  • http-response

  • httpie-request

  • request-body

  • response-body

您可以在设置过程中使用 RestDocumentationConfigurer API 更改默认代码片段配置。以下示例默认仅生成 curl-request 代码片段:

MockMvc
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
	.apply(documentationConfiguration(this.restDocumentation).snippets().withDefaults(curlRequest()))
	.build();
WebTestClient
this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
	.configureClient().filter(
		documentationConfiguration(this.restDocumentation)
			.snippets().withDefaults(curlRequest()))
	.build();
REST Assured
this.spec = new RequestSpecBuilder()
	.addFilter(documentationConfiguration(this.restDocumentation).snippets().withDefaults(curlRequest()))
	.build();

默认操作预处理器

您可以使用 RestDocumentationConfigurer API 在设置期间配置默认的请求和响应预处理器。以下示例从所有请求中删除 Foo 头,并对所有响应进行格式美化:

MockMvc
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
	.apply(documentationConfiguration(this.restDocumentation).operationPreprocessors()
		.withRequestDefaults(modifyHeaders().remove("Foo")) (1)
		.withResponseDefaults(prettyPrint())) (2)
	.build();
1 应用一个请求预处理器,删除名为 Foo 的头。
2 应用一个响应预处理器,对其内容进行格式美化。
WebTestClient
this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
	.configureClient()
	.filter(documentationConfiguration(this.restDocumentation)
		.operationPreprocessors()
			.withRequestDefaults(modifyHeaders().remove("Foo")) (1)
			.withResponseDefaults(prettyPrint())) (2)
	.build();
1 应用一个请求预处理器,删除名为 Foo 的头。
2 应用一个响应预处理器,对其内容进行格式美化。
REST Assured
this.spec = new RequestSpecBuilder()
	.addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors()
		.withRequestDefaults(modifyHeaders().remove("Foo")) (1)
		.withResponseDefaults(prettyPrint())) (2)
	.build();
1 应用一个请求预处理器,删除名为 Foo 的头。
2 应用一个响应预处理器,对其内容进行格式美化。

使用 Asciidoctor

本节描述了使用Asciidoctor的各个方面,这些方面与Spring REST Docs特别相关。

Asciidoc是一种文档格式。Asciidoctor是一种从Asciidoc文件(以.adoc结尾)生成内容(通常为HTML)的工具。

包含代码段

本节介绍如何包含 Asciidoc 代码段。

为操作包含多个代码段

您可以使用名为 operation 的宏来导入已为特定操作生成的所有或部分代码段。通过在项目的 构建配置 中包含 spring-restdocs-asciidoctor 来使其可用。

宏的目标是操作的名称。在其最简单的形式中,您可以使用宏来包含操作的所有代码段,如下例所示:

operation::index[]

您还可以使用操作宏支持 snippets 属性。 snippets 属性用于选择应包含的代码段。属性的值是一个逗号分隔的列表。列表中的每个条目都应该是要包含的代码段文件的名称(减去 .adoc 后缀)。例如,只能包含 curl、HTTP 请求和 HTTP 响应代码段,如下例所示:

operation::index[snippets='curl-request,http-request,http-response']

前面的示例等效于以下内容:

[[example_curl_request]]
== Curl 请求

include::{snippets}/index/curl-request.adoc[]

[[example_http_request]]
== HTTP 请求

include::{snippets}/index/http-request.adoc[]

[[example_http_response]]
== HTTP 响应

include::{snippets}/index/http-response.adoc[]
章节标题

对于每个使用 operation 宏包含的代码片段,都会创建一个带有标题的章节。以下内置代码片段提供了默认标题:

代码片段 标题

curl-request

Curl 请求

http-request

HTTP 请求

http-response

HTTP 响应

httpie-request

HTTPie 请求

links

链接

request-body

请求体

request-fields

请求字段

response-body

响应体

response-fields

响应字段

对于上表中未列出的代码片段,默认标题是通过将 - 字符替换为空格并将第一个字母大写来生成的。例如,名为 custom-snippet 的代码片段的标题将为“Custom snippet”。

您可以使用文档属性自定义默认标题。属性的名称应为 operation-{snippet}-title。例如,要将 curl-request 代码片段的标题自定义为“示例请求”,您可以使用以下属性:

:operation-curl-request-title: 示例请求

包含单个代码段

使用 include 宏 可以将单个代码段包含在文档中。 你可以使用 snippets 属性(由在 构建配置 中配置的 spring-restdocs-asciidoctor 自动设置)来引用代码段输出目录。 下面的例子展示了如何做到这一点:

include::{snippets}/index/curl-request.adoc[]

自定义表格

许多代码片段包含默认配置的表格。可以通过在包含代码片段时提供一些额外的配置或使用自定义代码片段模板来自定义表格的外观。

格式化列

Asciidoctor 对 表格列的格式化 有丰富的支持。如下例所示,您可以使用 cols 属性指定表格列的宽度:

[cols="1,3"] (1)
include::{snippets}/index/links.adoc[]
1 表格的宽度在其两列之间划分,第二列的宽度是第一列的三倍。

配置标题

您可以使用以 . 为前缀的行来指定表格的标题。以下示例展示了如何做到这一点:

.Links (1)
include::{snippets}/index/links.adoc[]
1 表格的标题将是 Links

避免表格格式问题

Asciidoctor 使用 | 字符来分隔表格中的单元格。如果您希望 | 出现在单元格内容中,这可能会导致问题。您可以通过使用反斜杠转义 | 来避免这个问题——换句话说,使用 \| 而不是 |

所有默认的 Asciidoctor 代码段模板都通过使用名为 tableCellContent 的 Mustache lamba 自动执行此转义。如果您编写自己的自定义模板,您可能希望使用此 lamba。以下示例展示了如何转义包含 description 属性值的单元格中的 | 字符:

| {{#tableCellContent}}{{description}}{{/tableCellContent}}

更多阅读

有关自定义表格的更多信息,请参阅Asciidoctor 用户手册的表格部分

使用 Markdown

本节描述了与 Spring REST Docs 相关的 Markdown 使用方面。

局限性

Markdown 最初是为网络写作人员设计的,因此它不像 Asciidoctor 那样适合编写文档。通常,这些局限性可以通过使用构建在 Markdown 之上的其他工具来克服。

Markdown 没有官方支持表格。Spring REST Docs 的默认 Markdown 代码段模板使用 Markdown Extra 的表格格式

包含代码段

Markdown 没有内置支持将一个 Markdown 文件包含在另一个文件中。要将生成的 Markdown 代码段包含在您的文档中,您应该使用支持此功能的其他工具。一个特别适合记录 API 的例子是 Slate

贡献

Spring REST Docs旨在帮助您轻松地为RESTful服务生成高质量的文档。然而,没有您的贡献,我们无法实现这一目标。

问题

您可以在Stack Overflow上使用spring-restdocs标签提问有关Spring REST Docs的问题。同样,我们鼓励您通过回答问题来帮助您的Spring REST Docs用户伙伴。

错误

如果您认为您发现了错误,请花点时间搜索现有问题。如果没有人报告过这个问题,请开一个新问题,详细描述这个问题,并且最好包含一个重现它的测试。

增强功能

如果您希望对Spring REST Docs进行增强,我们非常欢迎您提交拉取请求。源代码位于GitHub上。您可能需要搜索现有问题拉取请求,看看增强功能是否已经被提出。您可能还需要开一个新问题,在开始工作之前讨论可能的增强功能。