1. 简介
本文将探讨使用 CATS 自动化测试基于 OpenAPI 配置的 REST API。手动编写 API 测试既繁琐又耗时,而 CATS 通过自动生成并运行数百个测试来简化这一过程。
这不仅能减少手动工作量,还能在开发早期识别潜在问题,提升 API 可靠性。即使是简单的 API 也可能出现常见错误,CATS 能帮助我们高效地发现并解决这些问题。
虽然 CATS 适用于任何使用 OpenAPI 注解的应用程序,但我们将以基于 Spring 和 Jackson 的应用为例进行演示。
2. 用 CATS 简化测试
CATS 全称 Contract Auto Test Service(契约自动测试服务),这里的契约指 REST API 的 OpenAPI 规范。自动测试是一种使用随机数据或 API 操作返回数据(如 ID)的模糊测试。它是一个外部 CLI 工具,需要访问 API 的 URL 及其 OpenAPI 契约(文件或 URL)。
主要特性包括:
- ✅ 基于契约自动生成并运行测试
- ✅ 自动生成包含测试结果的 HTML 报告
- ✅ 简单的认证配置方式
由于测试是自动生成的,除了修改 OpenAPI 规范后需重新运行外,无需维护。
这对拥有大量接口的 API 尤其有用。 加上模糊测试功能,它能生成我们根本想不到的测试用例。
2.1. 安装 CATS
有几种安装方式。最简单的两种是下载运行 JAR 包 或二进制文件。我们选择二进制文件,因为它不需要安装配置 Java 环境,方便在任何地方运行测试。
下载后,必须将 cats 二进制文件添加到环境变量,才能全局运行。
2.2. 运行测试
运行 cats 至少需要指定两个参数:contract(契约)和 server(服务)。本例中 OpenAPI 规范位于 /api-docs:
$ cats --contract=http://localhost:8080/api-docs --server=http://localhost:8080
也可以将契约作为本地 JSON/YAML 文件传入:
$ cats --contract=api-docs.yml --server=http://localhost:8080
默认 CATS 会测试规范中所有路径,但可通过模式匹配限制范围:
$ cats --server=http://localhost:8080 --paths="/path/a*,/path/b"
这个参数在大型规范中分批测试特定路径时特别实用。
2.3. 添加认证头
通常我们的 API 需要某种认证。此时可在命令中添加认证头,以 Bearer 认证 为例:
$ cats --server=http://localhost:8080 -H "Authorization=Bearer a-valid-token"
2.4. 生成报告
运行后会在本地生成 HTML 报告:
稍后我们将分析部分错误,看看如何重构代码。
3. 项目设置
为演示 CATS,我们创建一个简单的 REST CRUD API,使用 @RestController 和 Bearer 认证。关键是要包含 @ApiResponse 注解,它们会在 OpenAPI 定义中添加重要信息(如媒体类型和未授权请求的预期状态码),CATS 会依赖这些信息:
@RestController
@RequestMapping("/api/item")
@ApiResponse(responseCode = "401", description = "Unauthorized", content = {
@Content(mediaType = MediaType.TEXT_PLAIN_VALUE, schema =
@Schema(implementation = String.class)
)
})
public class ItemController {
private ItemService service;
// 接口实现...
}
请求映射只定义了最少的 Swagger 注解,尽可能使用默认值:
@PostMapping
@ApiResponse(responseCode = "200", description = "Success", content = {
@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema =
@Schema(implementation = Item.class)
)
})
public ResponseEntity<Item> post(@RequestBody Item item) {
service.insert(item);
return ResponseEntity.ok(item);
}
// GET 和 DELETE 接口...
载荷类包含几个基本属性:
public class Item {
private String id;
private String name;
private int value;
// 默认 getter/setter...
}
4. 分析报告中的常见错误
我们分析报告中的部分错误,以便针对性解决。每个字段通常会有多个相似测试,这里只展示每种错误的详细页面。
4.1. 缺少推荐的安全头
OWASP 推荐了一组安全头。报告的详细测试页面显示了默认应包含的头信息:
Spring Security默认包含这些头,只需添加 spring-boot-starter-security 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.3.2</version>
</dependency>
无需在 SecurityFilterChain 中特殊配置安全头,只需定义一个简单的 JWT 配置,以便运行 cats 时传入有效令牌:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder())))
.build();
}
}
jwtDecoder() 的具体实现取决于需求,任何使用认证头的认证方式均可。
4.2. 请求字段超大值或越界值
当字段指定了最大长度时,CATS 会发送更长的值并期望服务器返回 4XX 状态码。未指定时默认最大长度为一万:
类似地,它会发送超大值并期望相同结果:
先自定义应用中的 ObjectMapper 解决这些问题。JsonFactoryBuilder 包含 StreamReadConstraints 配置,可设置字符串最大长度等约束。我们定义最大长度为 100:
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
JsonFactory factory = new JsonFactoryBuilder()
.streamReadConstraints(
StreamReadConstraints.builder()
.maxStringLength(100)
.build()
).build();
return new ObjectMapper(factory);
}
}
⚠️ 此最大长度需根据实际需求调整。更重要的是,这只能阻止应用接收超长请求,不会在 API 规范中定义约束。
要定义约束,需在载荷类中添加验证注解:
@Size(min = 37, max = 37)
private String id;
@NotNull
@Size(min = 1, max = 20)
private String name;
@Min(1)
@Max(100)
@NotNull
private int value;
边界值会影响 CATS 的测试生成策略。最后修改 POST 方法使用 @Valid 注解拒绝无效请求:
ResponseEntity<Item> post(@Valid @RequestBody Item item) {
//...
}
4.3. 格式错误的 JSON 和无效请求
默认情况下 Jackson 对请求非常宽松,甚至接受某些格式错误的 JSON:
回到 JacksonConfig,启用尾随令牌失败选项:
mapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
它还会接受混合字段(即包含 Item 类未定义的字段)、无效请求和空 JSON 体。启用未知属性反序列化失败即可解决:
mapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
4.4. 整数字段的浮点数
当属性为 int 时,Jackson 会截断浮点数值:
例如 0.34 会被截断为 0。禁用此功能避免问题:
mapper.disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT);
4.5. 值中的零宽度字符
部分模糊测试器会在字段名和值中插入零宽度字符:
*已启用 FAIL_ON_UNKNOWN_PROPERTIES,但仍需对字段值进行清理,移除零宽度字符。* 使用自定义 JSON 反序列化器,先定义包含零宽度字符正则表达式的工具类:
public class RegexUtils {
private static final Pattern ZERO_WIDTH_PATTERN =
Pattern.compile("[\u200B\u200C\u200D\u200F\u202B\u200E\uFEFF]");
public static String removeZeroWidthChars(String value) {
return value == null ? null
: ZERO_WIDTH_PATTERN.matcher(value).replaceAll("");
}
}
创建自定义反序列化器处理 String 字段:
public class ZeroWidthStringDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
return RegexUtils.removeZeroWidthChars(parser.getText());
}
}
再为 Integer 字段创建版本:
public class ZeroWidthIntDeserializer extends JsonDeserializer<Integer> {
@Override
public Integer deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
return Integer.valueOf(RegexUtils.removeZeroWidthChars(parser.getText()));
}
}
最后在 Item 字段中用 @JsonDeserialize 引用这些反序列化器:
@JsonDeserialize(using = ZeroWidthStringDeserializer.class)
private String id;
@JsonDeserialize(using = ZeroWidthStringDeserializer.class)
private String name;
@JsonDeserialize(using = ZeroWidthIntDeserializer.class)
private int value;
4.6. 错误请求响应与模式
经过上述修改,许多测试会返回“错误请求”,需在控制器中添加适当的 @ApiResponse 注解避免报告警告。由于错误请求的 JSON 响应由 Spring 的 BasicErrorController 动态处理,需创建一个类作为注解中的模式:
public class BadApiRequest {
private long timestamp;
private int status;
private String error;
private String path;
// 默认 getter/setter...
}
现在在控制器中添加定义:
@ApiResponse(responseCode = "400", description = "Bad Request", content = {
@Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = BadApiRequest.class)
)
})
5. 重构结果
重新运行报告后,错误数量减少超过 40%:
回顾我们解决的测试用例:
最终获得一个整体更安全的 API。
6. 实用子命令
CATS 提供子命令用于检查契约、重放测试等。这里介绍两个实用命令。
6.1. 检查 API
列出 API 规范中所有路径和操作:
$ cats list --paths -c http://localhost:8080/api-docs
命令按路径分组返回结果:
2 paths and 4 operations:
◼ /api/v1/item: [POST, GET]
◼ /api/v1/item/{id}: [GET, DELETE]
6.2. 重放测试
修复 Bug 时,replay 命令可重新运行特定测试:
cats replay Test216
从报告中获取测试编号替换命令中的值。每个测试的详细报告也包含完整的 replay 命令,可直接复制到终端执行。
7. 结论
本文介绍了如何使用 CATS 进行 OpenAPI 自动化测试,显著减少手动工作量并提升测试覆盖率。通过添加安全头、强制输入验证、配置严格反序列化等改进,示例应用的错误数量减少了 40% 以上。