该指南将引导你在 Spring 应用中为 HTTP 端点生成文档。
我们将使用一些暴露 API 的 HTTP 端点构建一个简单的 Spring 应用。我们将使用 JUnit 和 Spring 的 MockMvc
仅测试网络层。然后,我们将使用相同的测试通过 Spring REST Docs 生成 API 的文档。
像大多数的 Spring 入门指南一样,你可以从头开始并完成每个步骤,也可以绕过你已经熟悉的基本设置步骤。如论哪种方式,你最终都有可以工作的代码。
待一切就绪后,可以检查一下 gs-testing-restdocs/complete
目录中的代码。
对于所有的 Spring 应用来说,你应该从 Spring Initializr 开始。Initializr 提供了一种快速的方法来提取应用程序所需的依赖,并为你完成许多设置。该示例仅需要 Spring Web 依赖。下图显示了此示例项目的 Initializr 设置:
上图显示了选择 Maven 作为构建工具的 Initializr。你也可以使用 Gradle。它还将
com.example
和testing-restdocs
的值分别显示为 Group 和 Artifact。在本示例的其余部分,将用到这些值。
以下清单显示了选择 Maven 时创建的 pom.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>testing-restdocs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>testing-restdocs</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
以下清单显示了在选择 Gradle 时创建的 build.gradle
文件:
plugins {
id 'org.springframework.boot' version '2.2.0.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
为我们的 Spring 应用创建一个新的控制器。以下清单(来自 src/main/java/com/example/testingrestdocs/HomeController.java
)显示了如何执行该操作:
package com.example.testingrestdocs;
import java.util.Collections;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
@GetMapping("/")
public Map<String, Object> greeting() {
return Collections.singletonMap("message", "Hello, World");
}
}
Spring Initializr 创建一个可用于启动应用的主类。以下清单(来自 src/main/java/com/example/testingrestdocs/TestingRestdocsApplication.java
)显示了 Spring Initializr 创建的应用类:
package com.example.testingrestdocs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestingRestdocsApplication {
public static void main(String[] args) {
SpringApplication.run(TestingRestdocsApplication.class, args);
}
}
@SpringBootApplication
是一个便利的注解,它添加了以下所有内容:
@Configuration
:将类标注为应用上下文 Bean 定义的源;@EnableAutoConfiguration
:告诉 Spring Boot 根据类路径配置、其他 bean 以及各种属性的配置来添加 bean;@ComponentScan
:告知 Spring 在 com/example.testingrestdocs
包中寻找他组件、配置以及服务。main()
方法使用 Spring Boot 的 SpringApplication.run()
方法启动应用。
显示日志记录输出。该服务应在几秒内启动并运行。
现在该应用正在运行,我们可以对其进行测试。我们可以在 http://localhost:8080 加载主页。但是,为了使自己更有信心,在进行更改时该应用可以正常工作,我们希望自动化测试。我们还希望发布 HTTP 端点的文档。我们可以使用 Spring REST Docs 生成该测试的动态部分作为测试的一部分。
我们可以做的第一件事是编写一个简单的完整性检查测试,如果应用上下文无法启动,该测试将失败。为此,在测试范围内,将 Spring Test 和 Spring REST Docs 添加为项目的依赖。以下清单显示了使用 Maven 时要添加的内容。
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
以下清单显示了完整的 pom.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>testing-restdocs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>testing-restdocs</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
以下示例显示了使用 Gradle 时要添加的内容:
plugins {
id 'org.asciidoctor.convert' version '1.5.6'
}
asciidoctor {
sourceDir 'src/main/asciidoc'
attributes \
'snippets': file('target/snippets')
}
dependencies {
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
以下清单显示了完整的 build.gradle 文件:
plugins {
id 'org.springframework.boot' version '2.2.0.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
id 'org.asciidoctor.convert' version '1.5.6'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
asciidoctor {
sourceDir 'src/main/asciidoc'
attributes \
'snippets': file('target/snippets')
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
我们可以忽略构建文件中的注释。它们在那里是让我们挑选文件的一部分以包含在该指南中。
我们包含了 REST Docs 的
mockmvc
特色,它使用 Spring MockMvc 捕获 HTTP 内容。如果我们自己的应用不使用 Spring MVC,则还可以使用restassured
特色,该特色可用于完整的堆栈集成测试。
现在,使用 @RunWith
和 @SpringBootTest
注解以及一个空的测试方法创建一个测试用例,如下示例所示(来自 src/test/java/com/example/testingrestdocs/TestingRestdocsApplicationTests.java
)显示:
package com.example.testingrestdocs;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class TestingRestdocsApplicationTests {
@Test
public void contextLoads() throws Exception {
}
}
我们可以在 IDE 或命令行中运行该测试(通过运行 ./mvnw test
或 ./gradlew test
)。
进行完整性检查很好,但是我们还应该编写一些测试来断言我们的应用的行为。一种有用的方法是仅测试 MVC 层,在该层中,Spring 将处理传入的 HTTP 请求并将其交给我们的控制器。为此,我们可以使用 Spring 的 MockMvc
并通过在测试用例上使用 @WebMvcTest
注解要求将其注入。以下示例(来自 src/test/java/com/example/testingrestdocs/WebLayerTest.java
)显示了如何执行该操作:
@RunWith(SpringRunner.class)
@WebMvcTest(HomeController.class)
public class WebLayerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void shouldReturnDefaultMessage() throws Exception {
this.mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Hello, World")));
}
}
上一部分的测试发出(模拟)HTTP 请求并声明响应。我们创建的 HTTP API 具有动态内容(至少在原则上是这样),因此能够监视测试并从文档中提取 HTTP 请求的话,那就好了。Spring REST Docs 可以通过生成 “代码段” 来做到这一点。我们可以通过在测试中添加注释和额外的 “断言” 来使其工作。以下示例(来自 src/test/java/com/example/testingrestdocs/WebLayerTest.java
)显示了完整的测试:
package com.example.testingrestdocs;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HomeController.class)
@AutoConfigureRestDocs(outputDir = "target/snippets")
public class WebLayerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void shouldReturnDefaultMessage() throws Exception {
this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
.andExpect(content().string(containsString("Hello, World")))
.andDo(document("home"));
}
}
新的注释是 @AutoConfigureRestDocs
(来自 Spring Boot),它为生成的代码片段采用目录位置的参数。而新的断言是 MockMvcRestDocumentation.document
,它接受代码片段的字符串标识符的参数。
Gradle 用户可能更喜欢使用
build
而不是target
作为输出目录。但是,没关系。使用任何我们喜欢的方式。
运行测试,然后查看 target/snippets
。我们应该找到一个名为 home
(标识符)的目录,其中包含 Asciidocter 片段,如下所示:
└── target
└── snippets
└── home
└── curl-request.adoc
└── http-request.adoc
└── http-response.adoc
└── httpie-request.adoc
└── request-body.adoc
└── response-body.adoc
HTTP 请求和响应的默认代码段为 Asciidoctor 格式。还有 curl
和 httpie
的命令行示例(两个常见和流行的命令行 HTTP 客户端)。
我们可以通过向测试中的 document()
断言添加参数来创建其他代码段。例如,我们可以使用 PayloadDocumentation.responseFields()
代码段来记录 JSON 响应中的每个字段,如以下示例(来自 src/test/java/com/example/testingrestdocs/WebLayerTest.java
)所示:
this.mockMvc.perform(get("/"))
...
.andDo(document("home", responseFields(
fieldWithPath("message").description("The welcome message for the user.")
));
如果运行测试,则应该找到另一个名为 response-fields.adoc
的代码段文件。它包含一个字段名称和说明表。如果省略字段或输入错误的名称,则测试将失败。这就是 REST Docs 的强大之处。
我们可以创建自定义的代码段,并更改代码段的格式和自定义值,例如主机名。有关更多详细信息,请参见 Spring REST Docs 的文档。
要使用生成的代码片段,我们需要在项目中包含一些 Asciidoctor 内容,然后在构建时包含这些代码片段。要查看这项工作,请创建一个名为 src/main/asciidoc/index.adoc
的新文件,并根据需要添加代码段。以下示例(来自 src/main/asciidoc/index.adoc
)显示了如何执行该操作:
= Getting Started With Spring REST Docs
This is an example output for a service running at http://localhost:8080:
.request
include::{snippets}/home/http-request.adoc[]
.response
include::{snippets}/home/http-response.adoc[]
As you can see the format is very simple, and in fact you always get the same message.
该 Asciidoc 文件的主要功能是,通过使用 Asciidoctor include 指定(其中的冒号和尾括号告诉解析器在这些行上执行一些特殊操作),它包含两个摘录。注意,包含的代码片段的路径表示为占位符(Asciidoctor 中的一个属性),称为 {snippets}。在这种简单情况下,唯一的其他标记是顶部的 = (属于第一级部分标头)以及代码片段上的摘录(“请求”和“响应”)之后的点(.)。点(.)将该行上的文本变成标题。
然后,在构建配置中,我们需要将该源文件处理为所选的文档格式。例如,我们可以使用 Maven 生成 HTML(当我们执行 ./mvnw
包时,将生成 target/generated-docs
)。以下清单显示 pom.xml
文件的 Asciidoc 部分:
如果使用 Gradle,则在运行 ./gradlew asciidoctor
时会生成 build/asciidoc
。以下清单显示了 build.gradle
文件中与 Asciidoctor 相关的部分:
plugins {
...
id 'org.asciidoctor.convert' version '1.5.6'
}
...
asciidoctor {
sourceDir 'src/main/asciidoc'
attributes \
'snippets': file('target/snippets')
}
Gradle 中 Asciidoctor 源的默认位置是
src/doc/asciidoc
。我们将sourceDir
设置为与 Maven 的默认值匹配。
恭喜你!我们刚刚开发了一个 Spring 应用,并使用 Spring Restdocs 对其进行了文档记录。我们可以将创建的 HTML 文档发布到静态网站,也可以打包并让其对应用本身提供服务。我们的文档将始终是最新的,否则,测试将使我们的构建失败。
以下指南也可能会有所帮助:
想看指南的其他内容?请访问该指南的所属专栏:《Spring 官方指南》