# Spring REST Docs
将手写文档与通过 Spring MVC 测试、WebTestClient 或 Rest Assured 生成的自动生成的片段结合在一起来实现文档服务。
# 导言
Spring REST DOCS 的目的是帮助你为 RESTful 服务生成准确且可读的文档。
编写高质量的文档是困难的。减轻这一困难的一种方法是使用非常适合这项工作的工具。为此, Spring REST DOCS 默认使用ASCIIDoctor (opens new window)。ASCIIDoctor 处理纯文本并生成 HTML,并根据你的需要进行样式和布局。如果你愿意,还可以将 Spring REST DOCS 配置为使用 Markdown。
Spring REST DOCS 使用由用 Spring MVC 的、 Spring WebFlux 的[](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.0.x/ Spring-Framework-Reference/Testing.html#WebTestClient)或编写的测试产生的代码片段。这种测试驱动的方法有助于保证服务文档的准确性。如果代码片段不正确,则生成它的测试失败。
记录一个 RESTful 服务主要是描述它的资源。每个资源描述的两个关键部分是它使用的 HTTP 请求的详细信息和它产生的 HTTP 响应。 Spring REST DOCS 允许你使用这些资源以及 HTTP 请求和响应,从而保护你的文档不受服务实现的内部细节的影响。这种分离可以帮助你记录服务的 API,而不是它的实现。它还可以帮助你改进实现,而无需重新编写文档。
# 开始
本节描述了如何开始使用 Spring REST DOCS。
# 示例应用程序
如果你想直接进入,可以使用一些示例应用程序:
样本 | Build system | 说明 |
---|---|---|
Spring Data REST (opens new window) | Maven | 演示如何为使用Spring Data REST (opens new window)实现的服务创建入门指南和 API 指南。 |
Spring HATEOAS (opens new window) | Gradle | 演示如何为使用Spring HATEOAS (opens new window)实现的服务创建入门指南和 API 指南。 |
样本 | Build system | Description |
---|---|---|
WebTestClient (opens new window) | Gradle | Demonstrates the use of Spring REST docs with Spring WebFlux’s WebTestClient. |
样本 | Build system | 说明 |
---|---|---|
放心吧 (opens new window) | Gradle | Demonstrates the use of Spring REST Docs with 放心吧 (opens new window). |
Sample | Build system | Description |
---|---|---|
Slate (opens new window) | Gradle | Demonstrates the use of Spring REST Docs with Markdown andSlate (opens new window). |
TestNG (opens new window) | Gradle | Demonstrates the use of Spring REST Docs with TestNG (opens new window). |
JUnit 5 (opens new window) | Gradle | Demonstrates the use of Spring REST Docs with JUnit 5 (opens new window). |
# 所需经费
Spring REST DOCS 具有以下最低要求:
Java8
Spring 框架 5(5.0.2 或更高版本)
此外,spring-restdocs-restassured
模块需要 REST SUARD3.0。
# 构建配置
使用 Spring REST DOCS 的第一步是配置项目的构建。Spring HATEOAS (opens new window)和Spring Data REST (opens new window)样本分别包含一个build.gradle
和pom.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>1.5.8</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-webtestclient 或spring-restdocs-restassured 上添加依赖关系。 |
---|---|
2 | 添加 ASCIIDoctor 插件。 |
3 | 使用prepare-package 允许文档包含在包中。 |
4 | 添加spring-restdocs-asciidoctor 作为 ASCIIDoctor 插件的依赖项。这将自动配置在 snippets 文件中使用的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 | 为扩展 ASCIIDoctor 的依赖项声明asciidoctorExt 配置。 |
3 | 在asciidoctorExt 配置中添加对spring-restdocs-asciidoctor 的依赖关系。这将自动配置在你的 .adoc 文件中使用的snippets 属性,以指向build/generated-snippets 。它还将允许你使用 operation 块宏。 |
4 | 在testImplementation 配置中添加对spring-restdocs-mockmvc 的依赖关系。如果你想使用 WebTestClient 或 Rest Assured 而不是 MockMVC,请分别在spring-restdocs-webtestclient 或spring-restdocs-restassured 上添加依赖关系。 |
5 | 配置一个属性来定义生成的片段的输出位置。 |
6 | 配置test 任务,将 snippets 目录添加为输出。 |
7 | 配置asciidoctor 任务。 |
8 | 将 snippets 目录配置为输入。 |
9 | 为扩展配置asciidoctorExt 配置的使用。 |
10 | 使任务依赖于测试任务,以便在创建文档之前运行测试。 |
# 打包文档
你可能希望将生成的文档打包到项目的 JAR 文件中——例如,在 Spring 启动时将其用作静态内容 (opens new window)。要做到这一点,请配置项目的构建,以便:
文档是在构建 JAR 之前生成的。
生成的文档包含在 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 的[](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.0.x/ Spring-Framework-Reference/Testing.html#WebTestClient),或向你正在记录的服务发出请求。然后,它为请求和结果响应生成文档片段。
# 设置你的测试
你如何设置测试取决于你所使用的测试框架。 Spring REST DOCS 为 JUnit4 和 JUnit5 提供一流的支持。也支持其他框架,例如 TestNG,尽管需要稍多的设置。
# 设置你的 JUnit4 测试
当使用 JUnit4 时,生成文档片段的第一步是声明一个public``JUnitRestDocumentation
字段,该字段被注释为 JUnit@Rule
。下面的示例展示了如何做到这一点:
@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
默认情况下,JUnitRestDocumentation
规则会根据项目的构建工具自动配置一个输出目录:
Build tool | 输出目录 |
---|---|
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 | 通过使用MockMVCRestDocumentationConfigurer 配置MockMVC 实例。可以从 documentationConfiguration() 上的静态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() 方法获得该类的实例。 |
---|
放心吧
private RequestSpecification spec;
@Before
public void setUp() {
this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)) (1)
.build();
}
1 | 通过将RestAssuredRestDocumentationConfigurer 添加为Filter 来配置 Rest Assured。你可以在 RestAssuredRestDocumentation 包中从RestAssuredRestDocumentation 上的静态documentationConfiguration() 方法获得该类的实例。 |
---|
配置器应用合理的默认值,还提供了用于定制配置的 API。有关更多信息,请参见配置部分。
# 设置你的 JUnit5 测试
当使用 JUnit5 时,生成文档片段的第一步是将RestDocumentationExtension
应用到测试类。下面的示例展示了如何做到这一点:
@ExtendWith(RestDocumentationExtension.class)
public class JUnit5ExampleTests {
在测试典型的 Spring 应用程序时,还应该应用SpringExtension
:
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class JUnit5ExampleTests {
根据项目的构建工具,RestDocumentationExtension
会自动配置一个输出目录:
Build tool | 输出目录 |
---|---|
Maven | target/generated-snippets |
Gradle | build/generated-snippets |
如果你使用的是 JUnit5.1,那么你可以通过在你的测试类中将扩展注册为字段并在创建它时提供一个输出目录来覆盖默认的扩展。下面的示例展示了如何做到这一点:
public class JUnit5ExampleTests {
@RegisterExtension
final RestDocumentationExtension restDocumentation = new RestDocumentationExtension ("custom");
}
接下来,你必须提供一个@BeforeEach
方法来配置 MockMVC、WebTestClient 或 放心吧。下面的列表展示了如何做到这一点:
MockMVC
private MockMvc mockMvc;
@BeforeEach
public 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
public void setUp(ApplicationContext applicationContext, RestDocumentationContextProvider restDocumentation) {
this.webTestClient = WebTestClient.bindToApplicationContext(applicationContext).configureClient()
.filter(documentationConfiguration(restDocumentation)) (1)
.build();
}
1 | 将WebTestClient 实例配置为将WebTestClientRestDocumentationConfigurer 添加为ExchangeFilterFunction 。你可以从 org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation 上的静态documentationConfiguration() 方法获得该类的实例。 |
---|
放心吧
private RequestSpecification spec;
@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation)) (1)
.build();
}
1 | 通过将RestAssuredRestDocumentationConfigurer 添加为Filter 来配置 Rest Assured。你可以在 RestAssuredRestDocumentation 包中从RestAssuredRestDocumentation 上的静态documentationConfiguration() 方法获得该类的实例。 |
---|
配置器应用合理的默认值,还提供了用于定制配置的 API。有关更多信息,请参见配置部分。
# 在不使用 JUnit 的情况下设置测试
不使用 JUnit 时的配置在很大程度上与使用 JUnit 时的配置相似。这一节描述了主要的区别。测试样本 (opens new window)也说明了这种方法。
第一个区别是,你应该使用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());
}
放心吧
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();
}
# Invoking the RESTful Service
现在你已经配置了测试框架,你可以使用它来调用 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 的目录(位于配置的输出目录下面)。片段由 ExchangeResult 的Consumer 所写。你可以从 org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation 上的静态document 方法获得这样的使用者。 |
放心吧
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.restassured3 包中从RestAssuredRestDocumentation 上的静态document 方法获得该类的实例。 |
4 | 调用服务的根(/ )。 |
5 | 断言服务产生了预期的响应。 |
默认情况下,编写了六个片段:
<output-directory>/index/curl-request.adoc
<output-directory>/index/http-request.adoc
<output-directory>/index/http-response.adoc
<output-directory>/index/httpie-request.adoc
<output-directory>/index/request-body.adoc
<output-directory>/index/response-body.adoc
参见记录你的 API,以获取有关这些片段和可由 Spring REST DOCS 产生的其他片段的更多信息。
# 使用片段
在使用生成的代码片段之前,你必须创建一个.adoc
源文件。只要该文件有.adoc
后缀,你就可以为该文件命名任何你喜欢的名称。生成的 HTML 文件具有相同的名称,但带有.html
后缀。源文件和生成的 HTML 文件的默认位置取决于你是使用 Maven 还是 Gradle:
Build tool | Source files | 生成的文件 |
---|---|---|
Maven | src/main/asciidoc/*.adoc | target/generated-docs/*.html |
Gradle | src/docs/asciidoc/*.adoc | build/asciidoc/html5/*.html |
然后,可以使用包括宏 (opens new window)将生成的片段包括在手动创建的 ASCIIDoc 文件中(在本节前面描述)。可以使用snippets
属性,该属性由spring-restdocs-asciidoctor
中配置的构建配置自动设置,以引用片段输出目录。下面的示例展示了如何做到这一点:
include::{snippets}/index/curl-request.adoc[]
# 记录你的 API
本节提供了有关使用 Spring REST DOCS 来记录 API 的更多详细信息。
# Hypermedia
Spring REST DOCS 提供了对基于超媒体的 (opens new window)API 中的链接进行文档化的支持。以下示例展示了如何使用它:
MockMVC
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
.andDo(document("index", links((1)
linkWithRel("alpha").description("Link to the alpha resource"), (2)
linkWithRel("bravo").description("Link to the bravo resource")))); (3)
1 | 配置 Spring REST DOCS 以生成描述响应链接的片段。 在 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上使用静态links 方法。 |
---|---|
2 | 期望一个rel 是alpha 的链接。在 linkWithRel 上使用静态linkWithRel 方法。 |
3 | 期望一个链接的rel 是bravo 。 |
WebTestClient
this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk()
.expectBody().consumeWith(document("index", links((1)
linkWithRel("alpha").description("Link to the alpha resource"), (2)
linkWithRel("bravo").description("Link to the bravo resource")))); (3)
1 | 配置 Spring REST DOCS 以生成描述响应链接的片段。 在 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上使用静态links 方法。 |
---|---|
2 | 期望一个其rel 是alpha 的链接。在 linkWithRel 上使用静态linkWithRel 方法。 |
3 | 期望一个链接的rel 是bravo 。 |
放心吧
RestAssured.given(this.spec).accept("application/json").filter(document("index", links((1)
linkWithRel("alpha").description("Link to the alpha resource"), (2)
linkWithRel("bravo").description("Link to the bravo resource")))) (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 格式的链接但具有不同的内容类型,则可以提供一个内置的LinkExtractor
实现到links
。下面的例子说明了如何做到这一点:
MockMVC
.andDo(document("index", links(halLinks(), (1)
linkWithRel("alpha").description("Link to the alpha resource"),
linkWithRel("bravo").description("Link to the bravo resource"))));
1 | 指示链接为 HAL 格式。 在 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上使用静态halLinks 方法。 |
---|
WebTestClient
.consumeWith(document("index", links(halLinks(), (1)
linkWithRel("alpha").description("Link to the alpha resource"),
linkWithRel("bravo").description("Link to the bravo resource"))));
1 | 指示链接为 HAL 格式。 在 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上使用静态halLinks 方法。 |
---|
放心吧
.filter(document("index", links(halLinks(), (1)
linkWithRel("alpha").description("Link to the alpha resource"),
linkWithRel("bravo").description("Link to the bravo resource"))))
1 | 指示链接为 HAL 格式。 在 org.springframework.restdocs.hypermedia.HypermediaDocumentation 上使用静态halLinks 方法。 |
---|
如果你的 API 以 Atom 或 HAL 以外的格式表示其链接,则可以提供你自己的LinkExtractor
接口的实现,以从响应中提取链接。
# 忽略公共链接
在使用 HAL 时,你可能希望在概述部分对它们进行一次文档记录,然后在 API 文档的其余部分中忽略它们,而不是对每个响应都通用的链接进行文档记录,例如self
和curies
。为此,你可以构建支持重用代码片段,将链接描述符添加到预先配置为忽略某些链接的片段中。下面的示例展示了如何做到这一点:
public static LinksSnippet links(LinkDescriptor... descriptors) {
return HypermediaDocumentation.links(linkWithRel("self").ignored().optional(), linkWithRel("curies").ignored())
.and(descriptors);
}
# 请求和响应有效载荷
除了特定于超媒体的支持前面描述的外,还提供了对请求和响应有效负载的一般文档的支持。
默认情况下, Spring REST DOCS 自动生成用于请求的主体和响应的主体的片段。这些片段分别命名为request-body.adoc
和response-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("The user's email address"), (2)
fieldWithPath("contact.name").description("The user's name")))); (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("The user's email address"), (2)
fieldWithPath("contact.name").description("The user's name")))); (3)
1 | 配置 Spring REST DOCS 以生成描述响应负载中的字段的片段。 要记录请求,可以使用 requestFields 。都是 org.springframework.restdocs.payload.PayloadDocumentation 上的静态方法。 |
---|---|
2 | 期望具有路径contact.email 的字段。在 org.springframework.restdocs.payload.PayloadDocumentation 上使用静态fieldWithPath 方法。 |
3 | 期望具有路径contact.name 的字段。 |
放心吧
RestAssured.given(this.spec).accept("application/json").filter(document("user", responseFields((1)
fieldWithPath("contact.name").description("The user's name"), (2)
fieldWithPath("contact.email").description("The user's email address")))) (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("The user's contact details")))); (1)
1 | 用路径contact 记录该小节。contact.email 和contact.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("The user's contact details")))); (1)
1 | 用路径contact 记录该小节。contact.email 和contact.name 现在也被视为已被记录。在 org.springframework.restdocs.payload.PayloadDocumentation 上使用静态subsectionWithPath 方法。 |
---|
放心吧
RestAssured.given(this.spec).accept("application/json")
.filter(document("user",
responseFields(subsectionWithPath("contact").description("The user's contact details")))) (1)
.when().get("/user/5").then().assertThat().statusCode(is(200));
1 | 用路径contact 记录该小节。contact.email 和contact.name 现在也被视为已被记录。在 org.springframework.restdocs.payload.PayloadDocumentation 上使用静态subsectionWithPath 方法。 |
---|
subsectionWithPath
对于提供有效载荷的特定部分的高级概述很有用。然后,你可以为一个小节生成单独的、更详细的文档。见记录请求或响应有效负载的一个分节。
如果你根本不想记录某个字段或小节,可以将其标记为“忽略”。这可以防止它出现在生成的代码片段中,同时避免前面描述的故障。
你还可以在放松模式下记录字段,在这种模式下,任何未记录的字段都不会导致测试失败。为此,在org.springframework.restdocs.payload.PayloadDocumentation
上使用relaxedRequestFields
和relaxedResponseFields
方法。在记录一个特定的场景时,如果你希望只关注有效负载的一个子集,这可能会很有用。
默认情况下, 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 有效负载中,以下路径都存在:
Path | 价值 |
---|---|
a | 包含b 的对象 |
a.b | 包含三个对象的数组 |
['a']['b'] | 包含三个对象的数组 |
a['b'] | 包含三个对象的数组 |
['a'].b | 包含三个对象的数组 |
a.b[] | 包含三个对象的数组 |
a.b[].c | 包含字符串one 和two 的数组 |
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 试图通过检查有效负载来确定其类型。支持七种不同的类型:
Type | Description |
---|---|
array | 字段的每一次出现的值都是一个数组。 |
boolean | 字段的每个出现的值都是布尔(true 或false )。 |
object | 字段的每一次出现的值都是一个对象。 |
number | 字段的每一次出现的值都是一个数字。 |
null | 字段的每个出现的值是null 。 |
string | 字段的每一次出现的值都是一个字符串。 |
varies | 该场在有效载荷中多次出现,具有各种不同的类型。 |
还可以使用FieldDescriptor
上的type(Object)
方法显式地设置类型。文档中使用了提供的Object
方法的toString
的结果。通常,使用JsonFieldType
枚举的值之一。下面的例子说明了如何做到这一点:
MockMVC
.andDo(document("index", responseFields(fieldWithPath("contact.email").type(JsonFieldType.STRING) (1)
.description("The user's email address"))));
1 | 将字段的类型设置为String 。 |
---|
WebTestClient
.consumeWith(document("user",
responseFields(
fieldWithPath("contact.email")
.type(JsonFieldType.STRING) (1)
.description("The user's email address"))));
1 | 将字段的类型设置为String 。 |
---|
放心吧
.filter(document("user", responseFields(fieldWithPath("contact.email").type(JsonFieldType.STRING) (1)
.description("The user's email address"))))
1 | 将字段的类型设置为String 。 |
---|
# XML 有效负载
本节介绍如何使用 XML 有效负载。
# XML 字段路径
使用 XPath 描述 XML 字段路径。/
用于下降到子节点。
# XML 字段类型
在记录 XML 有效负载时,必须使用FieldDescriptor
上的type(Object)
方法为字段提供类型。文档中使用了所提供类型的toString
方法的结果。
# 重用字段描述符
除了对重用片段的一般支持外,请求和响应片段还允许使用路径前缀配置其他描述符。这使得请求或响应有效负载的重复部分的描述符可以创建一次,然后重用。
考虑返回一本书的端点:
{
"title": "Pride and Prejudice",
"author": "Jane Austen"
}
title
和author
的路径分别是title
和author
。
现在考虑一个返回一组图书的端点:
[{
"title": "Pride and Prejudice",
"author": "Jane Austen"
},
{
"title": "To Kill a Mockingbird",
"author": "Harper Lee"
}]
title
和author
的路径分别是[].title
和[].author
。单本书和书数组之间的唯一区别是,字段的路径现在有一个[].
前缀。
你可以创建描述符来记录一本书,如下所示:
FieldDescriptor[] book = new FieldDescriptor[] { fieldWithPath("title").description("Title of the book"),
fieldWithPath("author").description("Author of the book") };
然后,你可以使用它们来记录一本书,如下所示:
MockMVC
this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
.andDo(document("book", responseFields(book))); (1)
1 | 通过使用现有描述符,文档title 和author |
---|
WebTestClient
this.webTestClient.get().uri("/books/1").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("book",
responseFields(book))); (1)
1 | 通过使用现有描述符,文档title 和author |
---|
放心吧
RestAssured.given(this.spec).accept("application/json").filter(document("book", responseFields(book))) (1)
.when().get("/books/1").then().assertThat().statusCode(is(200));
1 | 通过使用现有描述符,文档title 和author |
---|
你还可以使用描述符记录一组图书,如下所示:
MockMVC
this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
.andDo(document("book", responseFields(fieldWithPath("[]").description("An array of books")) (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("An array of books")) (1)
.andWithPrefix("[].", book))); (2)
1 | 记录该数组。 |
---|---|
2 | 文档[].title 和[].author 使用带[]. 前缀的现有描述符 |
放心吧
RestAssured.given(this.spec).accept("application/json")
.filter(document("books", responseFields(fieldWithPath("[]").description("An array of books")) (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 上使用静态responseBody 和beneathPath 方法。为请求体产生一个片段。 可以使用 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 上使用静态responseBody 和beneathPath 方法。为请求主体生成一个片段。可以使用 requestBody 代替responseBody 。 |
---|
放心吧
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 | 生成包含响应体的一个分节的片段。 在 responseBody 和beneathPath 方法上使用静态org.springframework.restdocs.payload.PayloadDocumentation 。为请求体生成一个片段,可以使用 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
对象(high
和low
)的字段,如下所示:
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("The forecast high in degrees celcius"), (2)
fieldWithPath("low").description("The forecast low in degrees celcius"))));
1 | 在路径weather.temperature 下的响应有效负载的小节中生成一个描述字段的片段。在 beneathPath 上使用静态beneathPath 方法。 |
---|---|
2 | 记录high 和low 字段。 |
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("The forecast high in degrees celcius"), (2)
fieldWithPath("low").description("The forecast low in degrees celcius"))));
1 | 在路径weather.temperature 下的响应有效负载的小节中生成一个描述字段的片段。在 beneathPath 上使用静态beneathPath 方法。 |
---|---|
2 | 记录high 和low 字段。 |
放心吧
RestAssured.given(this.spec).accept("application/json")
.filter(document("location", responseFields(beneathPath("weather.temperature"), (1)
fieldWithPath("high").description("The forecast high in degrees celcius"), (2)
fieldWithPath("low").description("The forecast low in degrees celcius"))))
.when().get("/locations/1").then().assertThat().statusCode(is(200));
1 | 在路径weather.temperature 下的响应有效负载的小节中生成一个描述字段的片段。在 beneathPath 上使用静态beneathPath 方法。 |
---|---|
2 | 记录high 和low 字段。 |
结果是一个片段,其中包含一个表,该表描述high
和low
的weather.temperature
字段。为了使代码片段的名称不同,还包含了该小节的标识符。默认情况下,这个标识符是beneath-${path}
。例如,前面的代码会产生一个名为response-fields-beneath-weather.temperature.adoc
的代码片段。
# 请求参数
你可以使用requestParameters
记录请求的参数。你可以在GET
请求的查询字符串中包含请求参数。下面的例子说明了如何做到这一点:
MockMVC
this.mockMvc.perform(get("/users?page=2&per_page=100")) (1)
.andExpect(status().isOk()).andDo(document("users", requestParameters((2)
parameterWithName("page").description("The page to retrieve"), (3)
parameterWithName("per_page").description("Entries per page") (4)
)));
1 | 在查询字符串中,使用两个参数GET 和per_page 执行GET 请求。 |
---|---|
2 | 配置 Spring REST DOCS 以生成描述请求参数的片段。 在 org.springframework.restdocs.request.RequestDocumentation 上使用静态requestParameters 方法。 |
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", requestParameters((2)
parameterWithName("page").description("The page to retrieve"), (3)
parameterWithName("per_page").description("Entries per page") (4)
)));
1 | 在查询字符串中,使用两个参数GET 和per_page 执行GET 请求。 |
---|---|
2 | 配置 Spring REST DOCS 以生成描述请求参数的片段。 在 org.springframework.restdocs.request.RequestDocumentation 上使用静态requestParameters 方法。 |
3 | 记录page 参数。在 org.springframework.restdocs.request.RequestDocumentation 上使用静态parameterWithName 方法。 |
4 | 记录per_page 参数。 |
放心吧
RestAssured.given(this.spec).filter(document("users", requestParameters((1)
parameterWithName("page").description("The page to retrieve"), (2)
parameterWithName("per_page").description("Entries per page")))) (3)
.when().get("/users?page=2&per_page=100") (4)
.then().assertThat().statusCode(is(200));
1 | 配置 Spring REST DOCS 以生成描述请求参数的片段。 在 org.springframework.restdocs.request.RequestDocumentation 上使用静态requestParameters 方法。 |
---|---|
2 | 记录page 参数。在 org.springframework.restdocs.request.RequestDocumentation 上使用静态parameterWithName 方法。 |
3 | 记录per_page 参数。 |
4 | 在查询字符串中,使用两个参数GET 和per_page 执行GET 请求。 |
还可以将请求参数作为表单数据包含在 POST 请求的主体中。下面的例子说明了如何做到这一点:
MockMVC
this.mockMvc.perform(post("/users").param("username", "Tester")) (1)
.andExpect(status().isCreated()).andDo(document("create-user",
requestParameters(parameterWithName("username").description("The user's username"))));
1 | 使用单个参数POST 执行username 请求。 |
---|
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", requestParameters(
parameterWithName("username").description("The user's username")
)));
1 | 使用单个参数执行POST 请求,username 。 |
---|
放心吧
RestAssured.given(this.spec)
.filter(document("create-user",
requestParameters(parameterWithName("username").description("The user's username"))))
.formParam("username", "Tester") (1)
.when().post("/users") (2)
.then().assertThat().statusCode(is(200));
1 | 配置username 参数。 |
---|---|
2 | 执行POST 请求。 |
在所有情况下,结果都是一个名为request-parameters.adoc
的片段,其中包含一个表,该表描述了资源所支持的参数。
在记录请求参数时,如果在请求中使用了未记录的请求参数,则测试将失败。类似地,如果在请求中找不到已记录的请求参数,并且该请求参数未标记为可选的,则测试也会失败。
如果不想记录请求参数,可以将其标记为“忽略”。这可以防止它出现在生成的代码片段中,同时避免上述的故障。
你还可以在放松模式中记录请求参数,在这种模式中,任何未记录的参数都不会导致测试失败。要做到这一点,请在org.springframework.restdocs.request.RequestDocumentation
上使用relaxedRequestParameters
方法。在记录一个特定的场景时,如果你只想关注请求参数的一个子集,这可能会很有用。
# 路径参数
你可以使用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("The location's latitude"), (3)
parameterWithName("longitude").description("The location's longitude") (4)
)));
1 | 使用两个路径参数GET 和longitude 执行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("The location's latitude"), (3)
parameterWithName("longitude").description("The location's longitude")))); (4)
1 | 使用两个路径参数执行GET 请求,latitude 和longitude 。 |
---|---|
2 | 配置 Spring REST DOCS 以生成描述请求的路径参数的片段。 在 org.springframework.restdocs.request.RequestDocumentation 上使用静态pathParameters 方法。 |
3 | 记录名为latitude 的参数。在 org.springframework.restdocs.request.RequestDocumentation 上使用静态parameterWithName 方法。 |
4 | 记录名为longitude 的参数。 |
放心吧
RestAssured.given(this.spec).filter(document("locations", pathParameters((1)
parameterWithName("latitude").description("The location's latitude"), (2)
parameterWithName("longitude").description("The location's longitude")))) (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 | 使用两个路径参数GET 和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("The file to upload")) (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("The file to upload")) (3)
));
1 | 使用一个名为file 的部件执行POST 请求。 |
---|---|
2 | 配置 Spring REST DOCS 以生成描述请求部分的片段。 在 org.springframework.restdocs.request.RequestDocumentation 上使用静态requestParts 方法。 |
3 | 记录名为file 的部分。在 partWithName 上使用静态partWithName 方法。 |
放心吧
RestAssured.given(this.spec).filter(document("users", requestParts((1)
partWithName("file").description("The file to upload")))) (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 | 执行POST 请求到/upload 。 |
结果是一个名为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 的请求部分的主体的片段。在 requestPartBody 上使用静态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 方法。 |
---|
放心吧
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("The version of the image")))); (2)
1 | 配置 Spring REST DOCS 以生成描述在名为metadata 的请求部分的有效负载中的字段的片段。在 requestPartFields 上使用静态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("The version of the image")))); (2)
1 | 配置 Spring REST DOCS 以生成描述在名为metadata 的请求部分的有效负载中的字段的片段。在 requestPartFields 上使用静态requestPartFields 方法。 |
---|---|
2 | 期望具有路径version 的字段。在 fieldWithPath 上使用静态fieldWithPath 方法。 |
放心吧
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("The version of the image")))) (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 的请求部分的有效负载中的字段的片段。在 requestPartFields 上使用静态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 头
你可以通过分别使用requestHeaders
和responseHeaders
来记录请求或响应中的头。下面的例子说明了如何做到这一点:
MockMvc
this.mockMvc.perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) (1)
.andExpect(status().isOk()).andDo(document("headers", requestHeaders((2)
headerWithName("Authorization").description("Basic auth credentials")), (3)
responseHeaders((4)
headerWithName("X-RateLimit-Limit")
.description("The total number of requests permitted per period"),
headerWithName("X-RateLimit-Remaining")
.description("Remaining requests permitted in current period"),
headerWithName("X-RateLimit-Reset")
.description("Time at which the rate limit period will reset"))));
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("Basic auth credentials")), (3)
responseHeaders((4)
headerWithName("X-RateLimit-Limit")
.description("The total number of requests permitted per period"),
headerWithName("X-RateLimit-Remaining")
.description("Remaining requests permitted in current period"),
headerWithName("X-RateLimit-Reset")
.description("Time at which the rate limit period will reset"))));
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("Basic auth credentials")), (2)
responseHeaders((3)
headerWithName("X-RateLimit-Limit")
.description("The total number of requests permitted per period"),
headerWithName("X-RateLimit-Remaining")
.description("Remaining requests permitted in current period"),
headerWithName("X-RateLimit-Reset")
.description("Time at which the rate limit period will reset"))))
.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 头文件时,如果在请求或响应中找不到已记录的头文件,则测试将失败。
# 重用片段
对于正在文档中的 API 来说,具有一些在其多个资源中通用的特性是很常见的。为了避免在记录此类资源时出现重复,你可以重用配置有公共元素的Snippet
。
首先,创建描述公共元素的Snippet
。下面的示例展示了如何做到这一点:
protected final LinksSnippet pagingLinks = links(
linkWithRel("first").optional().description("The first page of results"),
linkWithRel("last").optional().description("The last page of results"),
linkWithRel("next").optional().description("The next page of results"),
linkWithRel("prev").optional().description("The previous page of results"));
其次,使用这个片段并添加更多特定于资源的描述符。下面的例子说明了如何做到这一点:
MockMvc
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
.andDo(document("example", this.pagingLinks.and((1)
linkWithRel("alpha").description("Link to the alpha resource"),
linkWithRel("bravo").description("Link to the bravo resource"))));
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("Link to the alpha resource"),
linkWithRel("bravo").description("Link to the bravo resource"))));
1 | 重用pagingLinks``Snippet ,调用and 以添加特定于被记录的资源的描述符。 |
---|
REST Assured
RestAssured.given(this.spec).accept("application/json").filter(document("example", this.pagingLinks.and((1)
linkWithRel("alpha").description("Link to the alpha resource"),
linkWithRel("bravo").description("Link to the bravo resource")))).get("/").then().assertThat()
.statusCode(is(200));
1 | 重用pagingLinks``Snippet ,调用and 以添加特定于被记录的资源的描述符。 |
---|
该示例的结果是,带有rel
值的first
、last
、next
、previous
、alpha
和bravo
的链接都是有文档记录的。
# 记录约束
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
](https://github.com/ Spring-projects/ Spring-restdocs/tree/v2.0.6.release/samples/rest-notes- Spring-hateoas/SRC/test/java/com/example/notes/apidocumentation.java)类显示了这一功能。
# 寻找约束
默认情况下,通过使用 Bean 验证Validator
来找到约束。目前,只支持属性约束。你可以通过使用自定义的ValidatorConstraintResolver
实例创建ConstraintDescriptions
来定制Validator
所使用的Validator
。要完全控制约束解析,可以使用自己的ConstraintResolver
实现。
# 描述约束
对于 Bean Validation2.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 的样本包含这样的资源包的一个例子 (opens new window)。
资源包中的每个键都是约束的完全限定名加上.description
。例如,标准@NotNull
约束的键是javax.validation.constraints.NotNull.description
。
你可以在约束的描述中使用引用约束属性的属性占位符。例如,@Min
约束的默认描述Must be at least ${value}
是指约束的value
属性。
要获得约束描述解析的更多控制,可以使用自定义ConstraintDescriptions
创建ResourceBundleConstraintDescriptionResolver
。要获得完全的控制,你可以使用自定义的ConstraintDescriptions
实现来创建ConstraintDescriptionResolver
。
# 在生成的代码片段中使用约束描述
一旦有了约束的描述,你就可以在生成的代码片段中随意使用它们。例如,你可能希望将约束描述作为字段描述的一部分。或者,你可以在请求字段片段中以额外信息的形式包含约束。 Spring 基于 Hateoas 的示例中的[ApiDocumentation
](https://github.com/ Spring-projects/ Spring-restdocs/tree/v2.0.6.release/samples/rest-notes- Spring-hateoas/SRC/test/java/com/example/notes/apidocumentation.java)类说明了后一种方法。
# 默认片段
当你记录请求和响应时,会自动生成许多代码片段。
Snippet | 说明 |
---|---|
curl-request.adoc | 包含[curl ](https://curl.haxx.se)命令,该命令与文档中的MockMvc 调用等价。 |
httpie-request.adoc | 包含[HTTPie ](https://httpie.org)命令,该命令相当于正在记录的MockMvc 调用。 |
http-request.adoc | 包含与正在记录的MockMVC 调用等价的 HTTP 请求。 |
http-response.adoc | 包含返回的 HTTP 响应。 |
request-body.adoc | 包含已发送的请求的主体。 |
response-body.adoc | 包含返回的响应的主体。 |
你可以配置默认情况下产生的代码段。有关更多信息,请参见配置部分。
# 使用参数化输出目录
当使用 MockMVC、Rest Assured 或WebTestClient
时,你可以参数化document
使用的输出目录。使用WebTestClient
参数化输出需要 Spring Framework5.3.5 或更高版本。
支持以下参数:
Parameter | 说明 |
---|---|
{methodName} | 测试方法的未修改名称。 |
{method-name} | 测试方法的名称,使用 kebab-case 格式化。 |
{method_name} | 测试方法的名称,使用 snake_case 格式化。 |
{ClassName} | 测试类的未修改的简单名称。 |
{class-name} | 测试类的简单名称,使用 kebab-case 格式化。 |
{class_name} | 测试类的简单名称,使用 Snake_case 格式化。 |
{step} | 当前测试中对服务的调用次数。 |
例如,document("{class-name}/{method-name}")
在一个名为creatingANote
的测试方法中,在测试类GettingStartedDocumentation
上将片段写入一个名为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();
}
放心吧
@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 使用模板来产生所生成的片段。是为 Spring REST DOCS 能够产生的每个片段提供的。要定制片段的内容,你可以提供自己的模板。
模板是从 Classpath 从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
方法向其添加一个或多个属性。在调用
curlRequest
、httpRequest
、httpResponse
时传入一些属性,以此类推。这样的属性与代码片段作为一个整体相关联。
在模板呈现过程中,任何附加属性都是可用的。再加上自定义的代码片段模板,这使得在生成的代码片段中包含额外的信息成为可能。
一个具体的例子是,在记录请求字段时,添加了一个约束列和一个标题。第一步是为你记录的每个字段提供constraints
属性,并提供title
属性。下面的例子说明了如何做到这一点:
MockMVC
.andDo(document("create-user", requestFields(attributes(key("title").value("Fields for user creation")), (1)
fieldWithPath("name").description("The user's name")
.attributes(key("constraints").value("Must not be null. Must not be empty")), (2)
fieldWithPath("email").description("The user's email address")
.attributes(key("constraints").value("Must be a valid email address"))))); (3)
1 | 为请求字段片段配置title 属性。 |
---|---|
2 | 为name 字段设置constraints 属性。 |
3 | 为email 字段设置constraints 属性。 |
WebTestClient
.consumeWith(document("create-user",
requestFields(
attributes(key("title").value("Fields for user creation")), (1)
fieldWithPath("name")
.description("The user's name")
.attributes(key("constraints").value("Must not be null. Must not be empty")), (2)
fieldWithPath("email")
.description("The user's email address")
.attributes(key("constraints").value("Must be a valid email address"))))); (3)
1 | 为请求字段片段配置title 属性。 |
---|---|
2 | 为name 字段设置constraints 属性。 |
3 | 为email 字段设置constraints 属性。 |
放心吧
.filter(document("create-user",
requestFields(attributes(key("title").value("Fields for user creation")), (1)
fieldWithPath("name").description("The user's name")
.attributes(key("constraints").value("Must not be null. Must not be empty")), (2)
fieldWithPath("email").description("The user's email address")
.attributes(key("constraints").value("Must be a valid email address"))))) (3)
1 | 为请求字段片段配置title 属性。 |
---|---|
2 | 为name 字段设置constraints 属性。 |
3 | 为email 字段设置constraints 属性。 |
第二步是提供一个名为request-fields.snippet
的自定义模板,该模板在生成的代码片段的表中包含有关字段约束的信息,并添加一个标题。下面的示例展示了如何做到这一点:
.{{title}} (1)
|===
|Path|Type|Description|Constraints (2)
{{#fields}}
|{{path}}
|{{type}}
|{{description}}
|{{constraints}} (3)
{{/fields}}
|===
1 | 将标题添加到表格中。 |
---|---|
2 | 添加一个名为“约束”的新列。 |
3 | 在表的每一行中包含描述符的constraints 属性。 |
# 定制请求和响应
在某些情况下,你可能不希望将请求记录为与发送的请求完全一致,或者不希望将响应记录为与接收的请求完全一致。 Spring REST DOCS 提供了许多可用于在请求或响应被文档化之前对其进行修改的预处理器。
通过调用document
和OperationRequestPreprocessor
或OperationResponsePreprocessor
来配置预处理。可以通过在Preprocessors
上使用静态preprocessRequest
和preprocessResponse
方法获得实例。下面的例子说明了如何做到这一点:
MockMVC
this.mockMvc.perform(get("/")).andExpect(status().isOk())
.andDo(document("index", preprocessRequest(removeHeaders("Foo")), (1)
preprocessResponse(prettyPrint()))); (2)
1 | 应用一个请求预处理程序,删除名为Foo 的标头。 |
---|---|
2 | 应用一个响应预处理器来打印它的内容。 |
WebTestClient
this.webTestClient.get().uri("/").exchange().expectStatus().isOk().expectBody()
.consumeWith(document("index",
preprocessRequest(removeHeaders("Foo")), (1)
preprocessResponse(prettyPrint()))); (2)
1 | 应用一个请求预处理程序来删除名为Foo 的头。 |
---|---|
2 | 应用一个响应预处理器来打印它的内容。 |
放心吧
RestAssured.given(this.spec).filter(document("index", preprocessRequest(removeHeaders("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(removeHeaders("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(removeHeaders("Foo")) (1)
.withResponseDefaults(prettyPrint())) (2)
.build();
}
1 | 应用一个请求预处理程序来删除名为Foo 的头。 |
---|---|
2 | 应用一个响应预处理器来打印它的内容。 |
放心吧
private RequestSpecification spec;
@Before
public void setup() {
this.spec = new RequestSpecBuilder()
.addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors()
.withRequestDefaults(removeHeaders("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"))));
放心吧
RestAssured.given(this.spec)
.filter(document("index", links(linkWithRel("self").description("Canonical self link")))).when()
.get("/").then().assertThat().statusCode(is(200));
通过Preprocessors
上的静态方法,可以获得各种内置的预处理器,包括上面所示的那些。有关更多详细信息,请参见below。
# 预处理器
# 漂亮的印刷
prettyPrint
onPreprocessors
格式化请求或响应的内容,以使其更易于阅读。
# 掩蔽链接
如果你正在记录一个基于超媒体的 API,你可能希望鼓励客户机通过使用链接而不是通过使用硬编码的 URI 来导航该 API。这样做的一种方法是限制在文档中使用 URI。maskLinks
onPreprocessors
将响应中任何链接的href
替换为…
。如果你愿意,你还可以指定一个不同的替代品。
# 删除页眉
removeHeaders
onPreprocessors
从请求或响应中删除任何标题,其中名称等于任何给定的标题名称。
removeMatchingHeaders
onPreprocessors
删除请求或响应中名称与给定正则表达式模式匹配的任何标题。
# 替换模式
replacePattern
onPreprocessors
提供了用于替换请求或响应中的内容的通用机制。任何与正则表达式匹配的事件都将被替换。
# 修改请求参数
你可以在Preprocessors
上使用modifyParameters
来添加、设置和删除请求参数。
# 修改 URI
如果使用 MockMVC 或未绑定到服务器的 WebTestClient,则应通过更改配置自定义 URI。 |
---|
你可以在Preprocessors
上使用modifyUris
来修改请求或响应中的任何 URI。当使用绑定到服务器的 Rest Assured 或 WebTestClient 时,这允许你在测试服务的本地实例时自定义文档中出现的 URI。
# 编写自己的预处理器
如果一个内置的预处理器不能满足你的需求,你可以通过实现OperationPreprocessor
接口来编写自己的预处理器。然后,你可以以与任何内置预处理器完全相同的方式使用你的定制预处理器。
如果只想修改请求或响应的内容(主体),可以考虑实现ContentModifier
接口,并将其与内置的ContentModifyingOperationPreprocessor
一起使用。
# 配置
本节介绍如何配置 Spring REST DOCS。
# 记录的 URI
本节介绍如何配置有文档的 URI。
# MockMVC URI 定制
在使用 MockMVC 时, Spring REST DOCS 记录的 URI 的默认配置如下:
Setting | 默认值 |
---|---|
Scheme | http |
Host | localhost |
Port | 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 SEART URI 定制
Rest Assured 通过发出实际的 HTTP 请求来测试服务。因此,在对服务执行操作之后,但在对其进行文档记录之前,必须对 URI 进行自定义。aRest Assured-Specific 预处理器用于此目的。
# WebTestClient URI 自定义
当使用 WebTestClient 时, Spring REST DOCS 记录的 URI 的默认基础是[http://localhost:8080](http://localhost:8080)
。你可以通过使用WebTestClient.Builder
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.0.x/javadoc-api/org/springframework/test/web/reactive/server/webtestclient.builder.html#baseurl-java.lang.string-)上的[<baseUrl(String)
方法来定制这个基础。下面的示例展示了如何做到这一点:
@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](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();
放心吧
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();
放心吧
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();
放心吧
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(removeHeaders("Foo")) (1)
.withResponseDefaults(prettyPrint())) (2)
.build();
1 | 应用一个请求预处理程序来删除名为Foo 的标头。 |
---|---|
2 | 应用一个响应预处理器来打印它的内容。 |
WebTestClient
this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
.configureClient()
.filter(documentationConfiguration(this.restDocumentation)
.operationPreprocessors()
.withRequestDefaults(removeHeaders("Foo")) (1)
.withResponseDefaults(prettyPrint())) (2)
.build();
1 | 应用一个请求预处理程序,删除名为Foo 的标头。 |
---|---|
2 | 应用一个响应预处理器来打印它的内容。 |
放心吧
this.spec = new RequestSpecBuilder()
.addFilter(documentationConfiguration(this.restDocumentation).operationPreprocessors()
.withRequestDefaults(removeHeaders("Foo")) (1)
.withResponseDefaults(prettyPrint())) (2)
.build();
1 | 应用一个请求预处理程序,删除名为Foo 的标头。 |
---|---|
2 | 应用一个响应预处理器来打印它的内容。 |
# 与 ASCIIDoctor 合作
本节描述了与 Spring REST DOCS 特别相关的与 ASCIIDoctor 合作的方面。
ASCIIDoc 是文档格式。 ASCIIDoctor 是从 ASCIIDoc 文件(以 .adoc 结尾)生成内容(通常是 HTML)的工具。 |
---|
# Resources
# 包括片段
本节介绍如何包含 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 request
include::{snippets}/index/curl-request.adoc[]
[[example_http_request]]
== HTTP request
include::{snippets}/index/http-request.adoc[]
[[example_http_response]]
== HTTP response
include::{snippets}/index/http-response.adoc[]
# 章节标题
对于使用operation
宏包含的每个片段,都会创建一个带有标题的部分。为以下内建片段提供了默认标题:
片断 | Title |
---|---|
curl-request | Curl Request |
http-request | HTTP request |
http-response | HTTP response |
httpie-request | HTTPie request |
links | Links |
request-body | Request body |
request-fields | Request fields |
response-body | Response body |
response-fields | Response fields |
对于上表中未列出的片段,通过用空格替换-
字符并将第一个字母大写来生成默认标题。例如,一个名为custom-snippet``will be
的片段的标题是“自定义片段”。
你可以使用文档属性自定义默认标题。属性的名称应该是operation-{snippet}-title
。例如,要将curl-request
片段的标题自定义为“示例请求”,你可以使用以下属性:
:operation-curl-request-title: Example request
# 包括单个片段
包括宏 (opens new window)用于在文档中包含单个片段。可以使用snippets
属性(由构建配置中配置的spring-restdocs-asciidoctor
自动设置)来引用 snippets 输出目录。下面的示例展示了如何做到这一点:
include::{snippets}/index/curl-request.adoc[]
# 自定义表格
许多片段在其默认配置中包含一个表。表的外观可以自定义,可以在包含代码片段时提供一些附加配置,也可以使用自定义代码片段模板。
# 格式化列
ASCIIDoctor 对格式化表格的列 (opens new window)提供了丰富的支持。如下例所示,你可以使用cols
属性指定表中列的宽度:
[cols="1,3"] (1)
include::{snippets}/index/links.adoc[]
1 | 表格的宽度分为两列,第二列的宽度是第一列的三倍。 |
---|
# 配置标题
你可以使用以.
为前缀的行来指定表格的标题。下面的示例展示了如何做到这一点:
.Links (1)
include::{snippets}/index/links.adoc[]
1 | 表格的标题将是Links 。 |
---|
# 避免表格式问题
ASCIIDoctor 使用|
字符对表格中的单元格进行划界。如果你希望|
出现在单元格的内容中,这可能会导致问题。你可以通过使用反斜杠转义|
来避免这个问题——换句话说,使用\|
而不是|
。
所有默认的 ASCIIDoctor 片段模板都使用名为tableCellContent
的小胡子 lamba 自动执行这个转义。如果你编写自己的自定义模板,你可能想要使用这个 lamba。下面的示例展示了如何在包含description
属性的值的单元格中转义|
字符:
| {{#tableCellContent}}{{description}}{{/tableCellContent}}
# 进一步阅读
有关自定义表的更多信息,请参见ASCIIDoctor 用户手册中的表格部分 (opens new window)。
# 与 Markdown 合作
本节描述了与 Spring REST DOCS 特别相关的使用 Markdown 的方面。
# 局限性
Markdown 最初是为人们为网络写作而设计的,因此,它并不像 ASCIIDoctor 那样适合编写文档。通常,这些限制可以通过使用另一个构建在 Markdown 之上的工具来克服。
Markdown 没有对表格的官方支持。 Spring REST DOCS 的默认 Markdown 片段模板使用Markdown Extra 的表格格式 (opens new window)。
# 包括片段
Markdown 没有内置支持将一个 Markdown 文件包含在另一个文件中。要将生成的 Markdown 片段包含在文档中,你应该使用支持此功能的附加工具。一个特别适合于记录 API 的示例是Slate (opens new window)。
# 贡献
Spring REST DOCS 旨在使你能够轻松地为你的 RESTful 服务生成高质量的文档。然而,没有你们的贡献,我们就无法实现这一目标。
# Questions
通过使用spring-restdocs
标记,可以在堆栈溢出 (opens new window)上询问有关 Spring REST DOCS 的问题。同样,我们鼓励你通过回答问题来帮助你的同伴 Spring REST DOCS 用户。
# Bugs
如果你认为你发现了一个 bug,请花点时间搜索。如果没有其他人报告了该问题,请打开新的一期 (opens new window)详细描述该问题,并在理想情况下包含一个复制该问题的测试。
# 增强功能
如果你希望对 Spring REST DOCS 进行增强,那么最受欢迎的是拉请求。源代码位于GitHub (opens new window)上。你可能希望搜索现有问题 (opens new window)和拉请求 (opens new window),以查看是否已经提出了增强。你可能还希望打开新的一期 (opens new window)在开始工作之前讨论可能的增强。