翻译:
本章内容概述
- 编写基于代码的模式测试
- 自动生成 API 模式测试
- 使用验证代理进行模式测试
在上一章中,我探讨了如何通过从 API 服务生成 OpenAPI 定义,确保 OpenAPI 定义与服务行为保持一致。但在某些情况下,可能无法直接从服务生成 OpenAPI 定义。这可能是由于旧有 API 服务的技术限制或组织上的约束。即使可以从 API 服务生成 OpenAPI 定义,实际探索性测试中服务的行为也可能会暴露应用的一些意外行为,而这些可能未被 OpenAPI 定义覆盖。例如,一个包含非法字符的错误请求,可能导致后端应用服务器在请求被控制器错误处理逻辑处理前就返回错误。基于控制器代码生成的 OpenAPI 定义可能无法涵盖这种情况。
因此,增加一层验证来检查已发布的 API 参考文档是否与 API 行为一致,依然非常有价值。实现这一目标的一种方式是使用模式测试,利用 OpenAPI 定义作为 API 模式的规范。在本章中,我将讨论三种编写 OpenAPI 模式测试的技术:基于代码的模式测试、生成的模式测试和验证代理。
本章面向需要创建模式测试的开发人员,但 API 产品负责人也可以通过了解不同的模式测试方法获益。我将先介绍模式测试的基础知识,然后展示如何使用 Atlassian 的 Swagger Request Validator 库编写基于代码的模式测试。接着,为了演示如何从 OpenAPI 定义文件生成模式测试,我会展示两个工具------Schemathesis 和 Portman 的示例。最后,我会介绍如何将 Prism 验证代理集成到现有的端到端测试套件中。章节结尾会讨论模式测试与契约测试的区别。
本章示例代码位于示例项目的 chapter7 文件夹中。编写测试需要具备 Java 知识,但如果你没有 Java 背景,可以在 chapter7-completed 文件夹中找到完整示例。
7.1 模式测试
在讨论模式测试之前,先看看什么是 OpenAPI 模式。OpenAPI 规范使用模式对象来描述参数、请求或响应消息的数据类型,这种描述既对人类可读,也对机器可读。数据类型可以是基本类型(如整数或字符串)、数组或对象。下面是一个 API 响应中模式对象的示例,来自 OpenAPI 定义文件。它描述了一个简单的 Product 模式对象,只有两个字段------id 和 name:
yaml
responses:
"200":
description: OK #1
content:
application/json: #2
schema: #3
title: Product
required:
- id
type: object
additionalProperties: false #4
properties:
id:
type: string
format: uuid
description: Identifier for the product.
example: dcd53ddb-8104-4e48-8cc0-5df1088c6113
name:
maxLength: 50
minLength: 1
type: string
description: Name of the product.
example: "Acme Uber Dog Rope Toy"
- #1 响应描述
- #2 响应的媒体类型
- #3 响应的模式对象
- #4 响应只允许这里定义的属性
这个响应定义说明,200 成功响应返回 JSON 类型内容。模式中 required 字段表明 id 是必需的,而 name 是可选的。模式对象通过 type 字段定义数据类型,可能是 boolean、number、string、array 或 object。由于设置了 additionalProperties: false
,消息中只能有这里定义的两个属性 id 和 name。id 属性类型是字符串,并遵循 UUID 格式。name 也是字符串类型,长度限制在 1 到 50 个字符之间。
从 3.0 版本开始,OpenAPI 模式对象基于 JSON Schema 规范。因为模式对象描述了消息结构,所以它们非常适合自动化测试和验证客户端提交的数据。
对上述消息的模式测试,就是检查实际 API 返回的消息是否符合模式定义。这是一种自动化测试,会将 API 的响应与 API 定义中的模式对比,不匹配时测试失败。比如,上述模式测试会验证 name 属性是否为字符串类型且长度在 1 到 50 之间。在模式测试中,还可以检查 API 返回的内容类型和头信息是否符合 API 定义。
OpenAPI 定义包括请求和响应消息的模式。谈论 OpenAPI 文档中的模式测试时,我使用"消费者"表示发起 API 请求的客户端应用或服务,"提供者"表示提供 API 响应的服务。这个服务支持向 API 消费者提供数据或功能。基于此定义,模式测试就是确保提供者遵守已发布的提供者契约,如图 7.1 所示。

与功能测试不同,模式测试不检查副作用(例如,某个对象是否被持久化到数据库),也不检查响应消息中值的正确性(例如,某个整数属性的值是否为5或10)。模式测试也不是性能测试,因此不会对速度、稳定性或响应性进行任何检查。模式测试还不同于消费者驱动的契约测试。契约测试是一种更全面的技术,用于测试两个软件服务之间的集成点,因为它通过具体示例来检查消费者和提供者服务是否对它们在特定场景中交换的消息有共同理解。模式测试只检查API通信的消息数据类型是否与已发布的文档在某一时刻匹配,而契约测试(使用如 Pact 和 Spring Contracts 之类的工具)则走得更远,支持团队间的紧密协作以及通信服务随时间的演进。我会在后续章节中进一步讨论模式测试和契约测试的区别。
注:OpenAPI 定义可以被视为一种契约,有些线上资料会将模式测试称为契约测试,这可能会引起混淆,但在本书中,我将它们区别对待。Matt Fellows 有一篇很好的文章澄清了这些术语,链接:mng.bz/z8RQ。
接下来,我们来看确保 API 一致性的第一种模式测试技术:基于代码的模式测试。
7.2 基于代码的模式测试
假设你的用户反馈说,Product Catalog API 实际返回了 numberOfReviews
和 reviewRating
两个字段,但参考文档中没有包含这两个字段,尽管你的 OpenAPI 定义中使用了 additionalProperties: false
,明确规定响应中不应出现定义之外的额外字段。你该如何写一个模式测试来检查这个问题呢?
你可以编写一个自动化或基于代码的模式测试,从两个角度对 API 服务器的响应进行断言。第一种方式是基于模式,在代码中逐条编写断言。例如,断言某个返回的属性是字符串,且最大长度为 x。使用这种方式,如果 OpenAPI 定义中的模式发生变化,你需要记得手动更新测试代码。第二种方式是将 API 定义直接提供给测试,使用一个库来校验响应是否符合定义。你的测试代码可以是使用单元测试框架(例如 Java 的 JUnit)编写的独立测试文件,也可以是嵌入到请求集合中的测试代码,借助 API 客户端工具如 Postman 或 Insomnia 执行。
你可以调整提供者 API 的测试范围(即被测对象)。测试范围可以是完整部署的 API,包括 API 网关,这样模式测试作为端到端测试执行,涵盖整个系统栈;也可以局限于控制器层,只测试 API 服务本身,且依赖系统用模拟(mock)替代。图 7.2 展示了这种概念上的表示。

控制器级别的测试通过模拟依赖项运行,速度很快;而端到端测试则运行较慢,并且存在端到端测试固有的所有缺点:难以调试、维护和扩展。下面我们来看一个编写端到端模式测试和控制器级别 API 模式测试的示例。
7.2.1 编写端到端模式测试
假设你要编写一个基于代码的模式测试,针对你的 Product Catalog API 端点,验证响应是否符合 product-catalog-v1-0.oas.yaml
API 定义文件。
你可以使用 Rest Assured 测试库(github.com/rest-assure... Java 编写这个测试。Rest Assured 是一个用于测试 Java 中 REST 服务的领域特定语言(DSL),它提供了流畅的接口,使用 given/when/then 语法来表达 API 测试。为了对响应进行 OpenAPI 合约校验,可以使用开源的 Atlassian Swagger Request Validator 库(mng.bz/0Gn6)。
执行这个测试前,需要启动 Kong。Unix 类系统用户,进入 chapter7/kong
目录,运行 ./start_kong.sh
脚本启动 Kong API 网关。然后进入 chapter7/product-catalog-api-service
目录,运行 ./start_api_service.sh
启动 API 服务。
Windows 用户需要先在 kong.yaml
文件中将 host 值设置为 host.docker.internal
,然后在 chapter7/kong
目录运行 run_kong_docker.bat
启动 Kong。在 chapter7/product-catalog-api-service
目录运行 start_api_service.bat
启动服务。
API 网关和 API 服务启动后,就可以编写端到端模式测试了。进入 chapter7/product-catalog-api-service/src/test/java
文件夹,找到 EndToEndSchemaTest.java
文件。使用 Rest Assured DSL 向 http://localhost:8000/v1/catalog/products/{uuid}
(其中 {uuid}
为任意 UUID 值)发起 GET 请求,并检查返回状态码是否为 200,如下面代码示例所示。OpenApiValidationFilter
会检查返回响应是否符合 product-catalog-v1-0.oas.yaml
API 定义。
csharp
public class EndToEndSchemaTest {
private final OpenApiValidationFilter validationFilter = new
OpenApiValidationFilter("../product-catalog-v1-0.oas.yaml");
@Test
public void testGetProductEndToEnd() {
given() // #1
.header("X-API-Key", "my_secret_api_key") // #2
.filter(validationFilter) // #3
.when()
.get("http://localhost:8000/v1/catalog/products/612b4280-b5c0-4ad5-bce7-ede7ab2b90fc") // #4
.then()
.assertThat()
.statusCode(200); // #5
}
}
#1 使用 Rest Assured 的流式 DSL
#2 提供 API 网关要求的请求头
#3 使用过滤器验证响应是否符合 OpenAPI 定义
#4 向端口 8000 的 API 网关发起请求
#5 断言响应成功
运行测试,进入 chapter7/product-catalog-api-service
文件夹,运行 ./run_end_to_end_schema_test.sh
(Windows 下运行 ./run_end_to_end_schema_test.bat
)。你可能会看到如下错误:
less
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 1.58 s <<< FAILURE! -- in com.acmepetsupplies.productcatalog.EndToEndSchemaTest
[ERROR] testGetProductEndToEnd Time elapsed: 1.577 s <<< ERROR! com.atlassian.oai.validator.restassured.OpenApiValidationFilter$OpenApiValidationException:
{
"messages" : [ { "key" : "validation.response.body.schema.additionalProperties", "level" : "ERROR", "message" : "Object instance has properties which are not allowed by the schema: ["numberOfReviews","reviewRating"]", "context" : { "requestPath" : "/v1/catalog/products/612b4280-b5c0-4ad5-bce7-ede7ab2b90fc", "responseStatus" : 200, "location" : "RESPONSE", "pointers" : { "instance" : "/" }, "requestMethod" : "GET" } } ]
}
该测试表明你的 API 定义中将 additionalProperties
设置为 false
,但实际 API 返回了未在文档中声明的 numberOfReviews
和 reviewRating
字段。你需要更新测试,将 OpenApiValidationFilter
的参数改为使用包含这两个属性的 product-catalog-v1-1.oas.yaml
文件。重新运行测试,测试就会通过。
提示 :从安全角度看,建议将 additionalProperties
字段设置为 false
,这有助于检测 API 是否返回了意料之外的额外数据。有些网关允许上传 API 定义,并在运行时验证 API 流量,这样可以检测异常行为,比如攻击导致 API 返回不应有的额外数据。
因为该测试是从 API 消费者的视角验证 API 网关的响应,所以存在端到端测试的所有缺点。端到端测试随着测试数量增加,难以扩展,因为它们运行缓慢、失败时难以调试(涉及多个组件服务),且难以搭建和维护测试数据。所以要谨慎选择使用场景。
7.2.2 编写控制器模式测试
你也可以编写类似测试,而不必测试整个系统栈。Spring Boot 允许你使用 Spring 的 MockMvc 包测试应用控制器,无需启动整个服务器。MockMvc 处理你的 HTTP 请求并将其交给控制器处理。Rest Assured 有一个模块 spring-mock-mvc
,支持使用 MockMvc 单元测试控制器。
在 pom.xml
中添加 Rest Assured 的 spring-mock-mvc
依赖,同时添加 Atlassian 的 swagger-request-validator-mockmvc
依赖(mng.bz/QZ6G),该模块不仅... OpenAPI 文件验证请求和响应,还支持与 Spring MockMvc 集成:
xml
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<version>5.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlassian.oai</groupId>
<artifactId>swagger-request-validator-mockmvc</artifactId>
<version>${swagger-request-validator.version}</version>
</dependency>
然后在 src/test/java/com/acmesupplies/productcatalog
目录下,编写 ControllerSchemaTest
,对 ProductsApiController
的响应进行校验,如下示例。测试通过一个由 OpenAPI 定义生成的 ResultMatcher
验证响应。
java
package com.acmepetsupplies.productcatalog;
import com.acmepetsupplies.productcatalog.controller.ProductsApiController;
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static com.atlassian.oai.validator.mockmvc.OpenApiValidationMatchers.openApi;
import static io.restassured.module.mockmvc.RestAssuredMockMvc.given;
@SpringBootTest
public class ControllerSchemaTest {
@Test
public void testGetProductController() {
given()
.standaloneSetup(new ProductsApiController()) // #1
.when()
.get("/v1/catalog/products/612b4280-b5c0-4ad5-bce7-ede7ab2b90fc")
.then()
.assertThat()
.expect(openApi().isValid("../product-catalog-v1-0.oas.yaml")) // #2
.statusCode(200);
}
}
#1 使用 Spring MVC 基础设施注册并配置控制器
#2 使用匹配器校验响应是否符合 OpenAPI 定义
在 chapter7/product-catalog-api-service
目录下,运行 ./run_controller_schema_test.sh
(Windows 下运行 ./run_controller_schema_test.bat
)执行测试。你会看到之前类似的缺少字段的错误提示。
注意该测试执行非常快,因为它是控制器的单元测试。示例中 ProductApiController
没有依赖项,但如果有,也可以轻松模拟(mock)掉。由于是快速运行的单元测试,你可以测试"正常路径"(通常是 2xx 响应)和"异常路径"(通常是 4xx 和 5xx 响应)。不过该测试的缺点是不涵盖 API 网关的行为,比如发送错误的 API key 导致的 401 认证错误等。
7.3 代码级模式测试需要考虑的事项
你已经看到,代码级的模式测试可以写成端到端测试,也可以写成控制器测试。使用这种方式时需要考虑的一点是维护这些测试所需的工作量。每当你为 API 新增一个端点时,都需要为该端点的不同正常路径(happy path)和异常路径(sad path)响应消息添加测试。这会导致测试代码数量不断增长,给开发人员带来维护负担。
这种方式适合在你希望将多个 API 调用组合成一个测试场景时使用,从而触发应用逻辑中较深层次的部分,以返回处于某种复杂状态的对象。你可以通过编程方式创建 API 调用序列并测试响应。
在前面基于 Java 的示例中,你使用了 Atlassian Swagger Request Validator 库来编写模式测试。其他提供类似功能的开源库还包括 Hikaku Java 库(github.com/codecentric... JavaScript 的 jest-openapi 库(mng.bz/X1yv)。如前所述,... JavaScript 在 Postman 和 Insomnia 这类 API 平台工具中编写端到端 API 测试。有关在 Postman 中验证响应的示例,请参见:mng.bz/y8RB。
7.4 自动生成模式测试
与其为每个端点及其每种响应类型手动编写模式测试,不如直接从 OpenAPI 定义生成测试怎么样?通过这种方式,你可以使用工具从 API 定义中生成并执行正常路径和异常路径的模式测试,针对 API 网关或 API 服务进行验证。图 7.3 展示了这个过程的工作原理。你可以选择将生成的测试存储在版本控制中,或者存放到任何你希望的地方以备将来参考。

这是 Schemathesis 和 Portman 等工具所采用的方法。首先,我们来看一下 Schemathesis。
7.4.1 使用 Schemathesis 进行模式测试
Schemathesis(schemathesis.readthedocs.io)是一款基于属性的-s18dkzr25b3hhr6h5sobkmfh4a/) API 测试工具,可以根据提供的 API 定义文件生成并执行测试。它主要用于模糊测试(fuzz testing),同时也会验证 API 的模式。Schemathesis 使用 Python 编写,基于 Hypothesis 这个属性测试库构建,但它可以测试任何基于 OpenAPI 文档定义的 API,也支持 GraphQL API 测试。
假设你被要求编写一个模式测试,验证 Product Catalog API 是否符合 product-catalog-v1-0.oas.yaml 这个 API 定义文件。使用 Schemathesis,你需要运行 Schemathesis CLI,指向该 API 并提供 API 定义文件。
现在运行 Schemathesis CLI,并在执行测试之前,将如下策略作为预执行脚本传给 Schemathesis CLI,如示例清单 7.3 所示。(记住,你应先启动 API 网关,可以进入 chapter7/kong 目录运行 ./start_kong.sh
脚本;同时启动 API 服务,可以进入 chapter7/product-catalog-api-service 目录运行 ./start_api_service.sh
脚本。)这段命令指定 CLI 运行所有 Schemathesis 的检查,并将测试请求记录到 cassette.yaml
文件中。
清单 7.3 运行 Schemathesis
python
st run #1
--checks all #2
--contrib-openapi-formats-uuid #3
--cassette-path cassette.yaml #4
--data-generation-method all #5
--base-url http://localhost:8000 #6
--header "X-API-Key: my_secret_api_key" #7
product-catalog-v1-0.oas.yaml #8
#1 启动 Schemathesis 运行
#2 运行所有 Schemathesis 检查
#3 生成 UUID(用于路径 /v1/catalog/products/{id}
)
#4 保存 HTTP 请求和响应到文件
#5 生成正向和负向测试用例
#6 替换 OpenAPI 定义中的服务器 URL 为此基本 URL
#7 提供测试请求需要携带的头部信息
#8 用于生成测试的 OpenAPI 定义
提示:另一种便捷的方式是运行 chapter7 目录下的脚本 ./run_schemathesis_for_v1_0_api.sh
。
测试运行时应该会失败,出现包含如下信息的错误:
- Response violates schema
Additional properties are not allowed ('numberOfReviews', 'reviewRating' 是未预期的)
这表明 Schemathesis 帮你发现了模式问题!Schemathesis 在发送 API 请求时,会使用你在 OpenAPI 定义文件中提供的示例。查看 product-catalog-v1-0.oas.yaml
文件,你会发现为 productId
指定的示例 UUID c05aed25-97cd-4cbc-b299-3796538eee9c
,这个值也被用来发送请求,并记录在 cassette.yaml
文件中。因此,在 API 定义文件中提供示例值很重要,不仅方便你的用户阅读 API 文档,也便于自动化测试工具如 Schemathesis 使用。
提示:如果你想让 Schemathesis 忽略 OpenAPI 文档中的显式示例,改用生成的新值,可以添加命令行参数 --hypothesis-no-phases explicit
。
现在,改用修正了模式的 product-catalog-v1-1.oas.yaml
文件重新运行测试,可以执行 chapter7/run_schemathesis_for_v1_1_api.sh
脚本。你应该看到类似如下的结果:
ini
===============SUMMARY=================
Performed checks:
not_a_server_error 101 / 101 passed PASSED
status_code_conformance 101 / 101 passed PASSED
content_type_conformance 101 / 101 passed PASSED
response_headers_conformance 101 / 101 passed PASSED
response_schema_conformance 101 / 101 passed PASSED
Network log: cassette.yaml
这意味着什么?Schemathesis 根据 API 生成并运行了测试(此处为 101 个测试,实际数量可能不同)。每个测试执行了五种检查:
- 服务器返回 5xx HTTP 状态码时,标记测试为
not_a_server_error
失败。 - 返回的状态码不在 API 定义中时,标记为
status_code_conformance
失败。 - 返回的内容类型不在定义中时,标记为
content_type_conformance
失败。 - 返回的响应头未定义时,标记为
response_headers_conformance
失败。 - 响应体不符合定义的模式时,标记为
response_schema_conformance
失败。
查看生成的 chapter7/cassette.yaml
文件,可以看到 Schemathesis 运行的每个请求及其响应。该文件采用 VCR 格式(github.com/vcr/vcr),包含... http_interactions。每个 interaction 记录唯一 ID、测试时间、校验状态、请求和响应详情。以我的示例运行结果为例,我在 cassette.yaml
中发现如下内容:
json
{"id": "791e653b-32c4-4602-a2db-99a95cb5e78f", "status": 400,
"code":"validation.client_error", "title": "Bad Request",
"detail": "Illegal character CNTL=0x1b"}
注意该错误的 detail
字段提示请求中含有非法字符。作为模糊测试工具,Schemathesis 会生成包含非法字符的请求。API 模糊测试工具通过生成随机且部分格式错误的请求来发现 API 缺陷。通常情况下,Jetty(示例 Spring Boot 应用中的应用服务器)会对这种错误返回 400 错误 HTML 页面,这不符合 OpenAPI 中定义的响应格式。但我在 JettyCustomizer.java
类中定义了 CustomErrorHandler
,覆盖了默认行为,返回符合 API 定义的 application/json
格式消息。Schemathesis 这类模糊测试工具特别适合发现你可能遗漏的边界情况。
提示:你可以用命令行参数 --generation-allow-x00 false
配置 Schemathesis 禁止生成非法字符。
Schemathesis 非常适合验证你的 API 是否正确校验输入,因为它使用随机输入独立发起请求,并验证响应是否符合定义。但有些情况下,要成功发起 API 请求并测试响应模式,应用必须处于某种状态(由之前的 API 调用产生)。也就是说,你想组合测试多个 API 调用。比如,想获取用户档案,可能先需要调用创建用户接口,再用返回的用户 ID 调用获取档案接口。要在 Schemathesis 中运行这种有状态测试,你可以在 OpenAPI 3.0 文档中定义 link
对象来表示操作之间的关系。OpenAPI 的链接机制定义了响应与其它 API 操作之间的关系与遍历结构。(关于链接的详细讨论超出本书范围,可参考 Swagger 文档:swagger.io/docs/specif... 会用前一次响应的数据作为后续调用的输入,而非生成随机数据。
Schemathesis 使用 Python 编写,你也可以使用其程序接口写 Python 测试,而非仅用 CLI。Schemathesis 库提供了程序化 API,可以实现 CLI 的所有操作。
本节用 Schemathesis 举例,说明如何基于 OpenAPI 定义生成并运行模式测试。
还有一个同样采用该方法的有趣工具是 Portman,下一节我们来看。
7.4.2 使用 Portman 进行模式测试
Portman(github.com/apideck-lib... CLI 工具,可以生成并执行 Postman 集合(collection),用于检测你的 OpenAPI 定义是否与 API 实现一致。使用 Portman,你可以导入生成的集合到 Postman 桌面客户端,或上传到 Postman 工作区,还能直接执行集合。这样你就能把它集成进持续集成/持续交付(CI/CD)流水线。
用 Portman 针对 product-catalog-v1-1.oas.yaml
文件测试 API 的正常路径(200 成功案例),在 chapter7
目录运行以下命令:
bash
portman --cliOptionsFile portman/portman-cli.json
你会看到测试报告,包含类似如下输出:
less
Product Catalog API
❏ Products
↳ View a product's details
GET http://localhost:8000/v1/catalog/products/c05aed25-97cd-4cbc-b299-3796538eee9c [200 OK, 443B, 46ms]
✓ [GET]::/v1/catalog/products/:id - Status code is 2xx
✓ [GET]::/v1/catalog/products/:id - Schema is valid
✓ [GET]::/v1/catalog/products/:id - Content-Type is application/json
✓ [GET]::/v1/catalog/products/:id - Response has JSON Body
Collection run completed.
Portman 使用端点定义中的第一个示例值来运行测试。portman-cli.json
文件包含 Portman 的命令行选项,在本例中指定了 OpenAPI 定义文件位置和生成的 Postman 文件名,还引用了环境文件来存储生成集合所用的 Postman 变量。其他 CLI 选项包括:
runNewman
:使用内嵌的 Newman 运行器(github.com/postmanlabs...includeTests
:将 Portman 合规测试添加到生成的 Postman
集合中
baseUrl
:运行 Postman 集合时覆盖服务器 URL
下面是 portman-cli.json
文件示例:
json
{
"local": "product-catalog-v1-1.oas.yaml",
"output": "collection.postman.json",
"envFile": "portman/.env-portman",
"portmanConfigFile": "portman/portman-config.json",
"includeTests": true,
"syncPostman": false,
"runNewman": true,
"baseUrl": "http://localhost:8000"
}
portmanConfigFile
字段引用 Portman 的配置文件,里面定义了需要执行的合规测试。Portman 使用"contract tests"(契约测试)一词表示检查 API 是否符合 OpenAPI 定义的测试。本例中 portman/portman-config.json
文件规定了对所有定义的 API 操作,Portman 应检查状态码、模式、内容类型和响应头是否符合定义,还要检查响应内容是否为 JSON。配置文件示例如下:
json
{
"version": 1.0,
"overwrites": [
{
"openApiOperation": "*::/*",
"overwriteRequestHeaders": [
{
"key": "Host",
"value": "api.acme-pet-supplies.com",
"overwrite": true
}
]
}
],
"tests": {
"contractTests": [
{
"openApiOperation": "*::/*",
"statusSuccess": {
"enabled": true
}
},
{
"openApiOperation": "*::/*",
"schemaValidation": {
"enabled": true
}
},
{
"openApiOperation": "*::/*",
"contentType": {
"enabled": true
}
},
{
"openApiOperation": "*::/*",
"jsonBody": {
"enabled": true
}
},
{
"openApiOperation": "*::/*",
"headersPresent": {
"enabled": true
}
}
]
},
"globals": {
"securityOverwrites": {
"apiKey": {
"key": "X-API-Key",
"value": "{{apiKey}}"
}
}
}
}
注释:
- 指定对生成的 Postman 集合中的请求数据进行自定义修改
- 应用于所有 API 操作
- 覆盖请求头
- 覆盖 Host 头的值
- 检查响应状态码是否符合文档
- 检查响应模式是否符合文档
- 检查返回内容类型是否符合文档
- 检查响应体是否为 JSON
- 检查响应头是否存在
- 设置 API Key 头名称和值
- 将 API Key 头值设置为
.env-portman
文件中定义的PORTMAN_API_KEY
环境变量的值
提示:Portman CLI 有初始化命令帮助快速生成配置,试试运行 portman --init
,按照步骤创建配置。
Portman 能将模式测试生成为 Postman 集合,非常适合使用 Postman 集合的团队。它还支持将生成的集合同步到 Postman 工作区,方便共享。
7.5 使用校验代理确保一致性
另一种进行 API 合规性测试的方法是采用校验代理(validating proxy)模式。本节将介绍该模式及一个具体示例。
验证模式是否符合 API 行为的一种方式是将请求和响应通过一个中间服务器转发,由这个中间服务器根据预期的 API 定义来校验 API 服务器的响应。如果 API 服务器的响应与预期模式不匹配,中间服务器可以报告错误。为此,代理服务器需要引用包含响应模式的 OpenAPI 定义文件。图 7.4 对此进行了说明。

这种方法的一个优点是,通过将校验代理接入现有的 API 测试套件(尤其是端到端测试)就能轻松启动。但模式测试的覆盖范围仅限于端到端测试所覆盖的场景,例如,可能不会包含每个接口所有可能的异常路径场景。当然,如果你打算将其应用于端到端测试,就必须考虑所有固有的缺点(脆弱性、调试困难、难以扩展等)。
下面来看一个具体示例。假设之前提到的 Product Catalog API 定义文件中包含了 numberOfReviews
和 reviewRating
字段。使用校验代理的测试如何发现这个问题呢?首先,创建一个端到端测试 ProductsApiEndToEndTest
,它向 API 网关发起请求并检查是否返回了 200 OK 成功响应,代码示例如下:
java
package com.acmepetsupplies.productcatalog;
import org.junit.Test;
import static io.restassured.RestAssured.given;
public class ProductsApiEndToEndTest {
@Test
public void testGetProductEndToEnd() {
given()
.header("X-API-Key", "my_secret_api_key")
.when()
.baseUri("http://127.0.0.1:8000") // #1 基础 URI 指向 API 网关
.get("/v1/catalog/products/612b4280-b5c0-4ad5-bce7-ede7ab2b90fc")
.then()
.log() // #2 打印响应体
.body() // #2 打印响应体
.assertThat()
.statusCode(200);
}
}
运行测试,验证能否成功响应。进入 chapter7/product-catalog-api-service
目录,执行 ./run_products_api_end_to_end_test.sh
(Windows 下运行 ./run_products_api_end_to_end_test.bat
)。输出中会打印响应体。
接下来,你需要将端到端测试的接口地址改为指向校验代理。Prism(github.com/stoplightio... OpenAPI 定义和 Postman 集合的校验代理和模拟服务器。如果你按照附录 D 的说明操作,应该已经安装了 Prism。进入 chapter7
文件夹,启动 Prism,指定你的 OpenAPI 定义文件和 API 网关地址,并添加 --errors
标志以便在响应违规时返回错误:
arduino
prism proxy product-catalog-v1-0.oas.yaml http://127.0.0.1:8000 --errors
Prism 默认运行在 4010 端口。启动后,将 ProductsApiEndToEndTest
中的 baseUri
从 http://127.0.0.1:8000
改为 http://127.0.0.1:4010
,这样测试请求就会发到 Prism,而不是 API 网关。再次运行 ./run_products_api_end_to_end_test.sh
,测试应该会失败。响应体日志会显示如下信息:
json
{
"type": "https://stoplight.io/prism/errors#VIOLATIONS",
"title": "Request/Response not valid",
"status": 500,
"detail": "Your request/response is not valid and the --errors flag is set, so Prism is generating this error for you.",
"validation": [
{
"location": ["response", "body"],
"severity": "Error",
"code": "additionalProperties",
"message": "must NOT have additional properties; found 'reviewRating'"
},
{
"location": ["response", "body"],
"severity": "Error",
"code": "additionalProperties",
"message": "must NOT have additional properties; found 'numberOfReviews'"
}
]
}
Java 抛出断言错误:
erlang
java.lang.AssertionError: 1 expectation failed.
Expected status code <200> but was <500>.
Prism 的错误符合 RFC7807 "问题详情"格式,包含 type
(标识问题类型的 URI)、title
(问题类型简短描述)、status
(HTTP 状态码)和 detail
(对本次问题的解释)。这里 Prism 返回了类型为 VIOLATION 的 500 错误,因为 API 网关返回的响应与 API 定义不符,而 Prism 以 --errors
模式运行。validation.location
和 validation.message
字段详细说明了错误发生的定义位置和原因。
此外,Prism 错误日志还提供了与上游 API 网关的请求响应交互详情:
vbscript
[07:42:50] › [HTTP SERVER] get /v1/catalog/products/9764d95c-7757-4a4d-a800-660b8fe9392b ℹ info Request received
[07:42:50] › [PROXY] ℹ info Forwarding "get" request to http://localhost:8000/v1/catalog/products/9764d95c-7757-4a4d-a800-660b8fe9392b...
[07:42:50] › [PROXY] ℹ info The upstream call to /v1/catalog/products/9764d95c-7757-4a4d-a800-660b8fe9392b has returned 200
[07:42:50] › [HTTP SERVER] get /v1/catalog/products/9764d95c-7757-4a4d-a800-660b8fe9392b ✖ error Request terminated with error: https://stoplight.io/prism/errors#VIOLATIONS: Request/Response not valid
你也可以用匹配上游服务器的 API 定义文件测试 Prism 的表现。先停止当前运行的 Prism 实例,然后使用 product-catalog-v1-1.oas.yaml
(包含修正的模式)重新启动:
arduino
prism proxy product-catalog-v1-1.oas.yaml http://127.0.0.1:8000 --errors
再运行 ./run_products_api_end_to_end_test.sh
,你会看到测试通过。
7.6 比较 API 一致性检测方法
本章中,我们探讨了多种编写模式测试的技术。表 7.1 汇总了这些方法之间的一些权衡。
表 7.1 API 模式检测方法比较
方法 | 代码驱动端到端模式测试 | 代码驱动控制器模式测试 | 带模糊测试的自动生成端到端模式测试 | 使用校验代理的端到端模式测试 |
---|---|---|---|---|
适用于现有端到端测试套件,无需编写新代码 | 不适用 | 不适用 | 不适用 | 适用 |
保证整个栈响应符合模式 | 适用 | 不适用 | 适用 | 适用 |
可基于模糊测试识别意外响应 | 不适用 | 不适用 | 适用 | 如果作为模糊测试的一部分运行 |
测试服务单独运行时反馈迅速 | 不适用 | 适用 | 不适用 | 不适用 |
新响应或接口无需修改代码即可测试 | 不适用 | 不适用 | 适用。测试可仅基于 API 参考生成,但可能需要自定义配置。 | Prism 本身无需改动,但端到端测试本身需要修改以测试新接口或响应 |
代码生成 API 定义(第6章讨论)和模式测试都能解决 API 一致性问题。表 7.2 总结了这两种方法的权衡。在实际应用中,你可能需要结合两者以确保良好的 API 一致性。
表 7.2 生成与模式测试方法比较
维度 | 从代码生成 OpenAPI 定义 | 针对 OpenAPI 定义的模式测试 |
---|---|---|
执行速度 | 构建过程快速生成 | 控制器级别可快速,端到端测试则较慢 |
覆盖范围 | 仅覆盖生成时的 API 服务控制器模式,不涵盖 API 网关或防火墙、中间代理等组件行为 | 对消费者收到的响应做全面断言,覆盖整个调用链 |
维护成本 | 低。如果集成到 CI/CD 流水线,任何变更都能自动重新生成定义 | 如果作为端到端测试,维护成本和端到端测试一致,可能较高 |
入门难度 | 当一个 API 概念由多个服务共同提供时,需要合并多个生成的定义 | 可能需要在 API 控制器和响应模型代码中添加注解 |
7.7 模式测试与消费者驱动契约测试的区别
本章开头提到,模式测试不同于消费者驱动的契约测试。先解释什么是契约测试,再说明二者差异。
假设一个系统中有多个不断演进、相互交互的服务。如何验证新版本服务仍能与其他服务正常交互?一种方式是部署所有服务并运行集成测试。但随着服务数量增加,集成测试变得复杂。运行集成测试时,你需要先部署并配置所有服务("部署整个世界"),在接近生产环境的环境中进行。集成测试运行时间长,容易脆弱,难维护且难扩展。
消费者驱动契约测试是另一种集成测试方法。它在隔离环境中针对"消费者契约"测试服务------该契约是定义消费者期望提供者支持的消息交互的文档。即保证两个服务对通信方式有共同理解。它通过使用具体消息(规范示例)单独测试各服务。包含这些消息的文档即为契约,定义了预期的消息交互,如图 7.5 所示。

在消费者驱动的契约测试中,消费者通过消费者契约来传达其对提供者 API 行为的期望。消费者契约记录了消息交互------即为将提供者服务带到某一特定状态所需的消息序列或历史。在消息交换过程中,仅测试消费者使用的提供者功能。这意味着未被使用的提供者功能可以在不破坏测试的情况下进行演进。消费者契约有助于促进团队之间就服务演进开展讨论与协作。契约测试工具的两个示例是 Pact(pact.io)和-cj4e/) Spring Cloud Contracts(spring.io/projects/sp... A 返回响应 B)的消费者契约不同于仅描述 API 结构和格式的 API 定义文件。API 定义文件的模式测试则用于验证提供者的行为是否符合其公开的模式。
小贴士
更多关于消费者驱动契约测试的内容,请参见 Mark Winteringham 的《Testing Web APIs》(Manning,2022;www.manning.com/books/testi...
消费者驱动契约测试适合测试组织内服务之间的集成。它可以替代许多测试组织内部服务集成的端到端测试(例如微服务集成测试)。在这种情况下,提供者团队可以管理与少量已知 API 消费者应用的关系。
而针对 API 定义文件的模式测试更适合用于外部 API,此类 API 不适合使用消费者契约。对于外部 API,消费者数量可能众多,且消费者团队通过消费者契约驱动个别需求在实际中并不现实------消费者团队甚至可能未使用与提供者团队相同的契约测试工具。在缺少外部 API 的消费者契约情况下,发布准确描述提供者接口、功能和消息模式的 API 定义文件就显得尤为重要。提供者的模式测试有助于确保 API 定义的准确性。
总结
- 模式测试用于检查 API 是否符合其 API 定义,适用于无法应用消费者驱动契约测试的场景,例如外部或公共 API。
- 基于代码的模式测试需要编写测试代码,断言提供者返回消息的数据类型是否与其公开的 API 定义中的模式一致。
- 基于代码的模式测试既可以做端到端测试,也可以做 API 服务控制器级别的测试。控制器级测试更快,避免了端到端测试的一些缺点。
- 从 OpenAPI 定义生成模式测试,避免了 API 变更时需要开发者手写测试代码。工具如 Schemathesis 和 Portman 可以根据 API 定义自动生成并运行正常和异常路径的测试场景,并对 API 服务端进行测试。
- 模式校验代理可以置于自动化测试套件和 API 服务端之间,校验服务端响应是否符合模式。由于可以将代理集成进现有端到端测试套件,这是一种快速上手模式测试的方法。
- 结合生成 OpenAPI 定义和模式测试,可以有效保证外部 API 的一致性。