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 报告:

CATS 报告示例

稍后我们将分析部分错误,看看如何重构代码。

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 推荐了一组安全头。报告的详细测试页面显示了默认应包含的头信息:

CATS 安全头检查

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 状态码。未指定时默认最大长度为一万:

CATS 边界值测试

类似地,它会发送超大值并期望相同结果:

CATS 超大值测试

先自定义应用中的 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

CATS 格式错误 JSON 测试

回到 JacksonConfig,启用尾随令牌失败选项:

mapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);

它还会接受混合字段(即包含 Item 类未定义的字段)、无效请求空 JSON 体。启用未知属性反序列化失败即可解决:

mapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

4.4. 整数字段的浮点数

当属性为 int 时,Jackson 会截断浮点数值:

CATS 浮点数测试

例如 0.34 会被截断为 0。禁用此功能避免问题:

mapper.disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT);

4.5. 值中的零宽度字符

部分模糊测试器会在字段名和值中插入零宽度字符:

CATS 零宽度字符测试

*已启用 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%:

重构结果对比

回顾我们解决的测试用例:

  • 现在包含默认安全头: 安全头测试通过
  • 拒绝格式错误的 JSON: 格式错误 JSON 测试通过
  • 清理输入数据: 零宽度字符测试通过

最终获得一个整体更安全的 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% 以上。


原始标题:Automated Testing for OpenAPI Endpoints Using CATS | Baeldung