使用 CATS 對 OpenAPI 端點進行自動化測試
一、簡介
在本教學中,我們將探索使用CATS自動測試使用 OpenAPI 設定的 REST API。手動編寫 API 測試可能既乏味又耗時,但 CATS 透過自動產生和執行數百個測試來簡化流程。
透過在開發早期識別潛在問題,可以減少手動工作量並提高 API 可靠性。即使是簡單的 API,也可能會出現常見錯誤,而 CATS 可以幫助我們有效地找到並解決這些錯誤。
雖然 CATS 可與任何 OpenAPI 註釋的應用程式搭配使用,但我們將使用基於 Spring 和 Jackson 的應用程式來示範它。
2. 使用 CATS 使測試變得容易
CATS 代表合約自動測試服務。該合約引用了我們的 REST API 的 OpenAPI 規格。自動測試是一種模糊測試,使用隨機資料和某些場景下 API 操作傳回的資料(例如 ID)。它是一個外部 CLI 應用程序,需要以文件或 URL 的形式存取我們的 API 的 URL 及其 OpenAPI 合約。
其一些主要功能包括:
- 基於API Contract自動產生和執行測試
- 自動產生詳細說明測試結果的 HTML 報告
- 簡單配置滿足授權需求
由於測試是自動產生的,因此除了在更改 OpenAPI 規範時重新執行生成器之外,無需進行任何維護。
這對於具有許多端點的 API 尤其方便。由於它包含模糊測試,因此它會產生我們一開始從未考慮過的測試。
2.1.安裝CATS
我們有一些安裝選項。最簡單的兩個是下載並運行JAR或二進位。我們將選擇二進位選項,因為它不需要安裝和配置 Java 的環境,從而可以更輕鬆地從任何地方執行測試。
下載後,我們必須將cats
位檔案新增至我們的環境路徑中,以便從任何地方運行它。
2.2.運行測試
我們需要指定至少兩個參數來執行cats
: contract
和server
。在我們的例子中,OpenAPI 規範 URL 位於/api-docs
:
$ cats --contract=http://localhost:8080/api-docs --server=http://localhost:8080
我們也可以將合約作為包含規範的 JSON 或 YAML 本機檔案傳遞。
讓我們檢查一個範例,其中該檔案位於運行 CATS 的相同目錄中:
$ 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,我們將從具有@RestController
和 Bearer 驗證的簡單 REST CRUD API 開始。包含@ApiResponse
註釋非常重要,因為這些註釋包含 CATS 使用的 OpenAPI 定義中的重要詳細信息,例如媒體類型和未經授權請求的預期狀態代碼:
@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;
// endpoints ...
}
我們的請求映射定義了最少數量的 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 and DELETE endpoints ...
對於我們的有效負載類,我們將包含一些基本屬性:
public class Item {
private String id;
private String name;
private int value;
// default getters and setters...
}
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
配置,我們可以使用它來設定一些約束,包括String
的最大長度。讓我們定義最大長度為 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 會截斷為零。為了避免這種情況,讓我們關閉這個功能:
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()));
}
}
最後,我們使用@JsonDeserialize
註解在Item
欄位中引用這些反序列化器:
@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;
// default getters and setters...
}
現在,我們可以在控制器中包含另一個定義:
@ApiResponse(responseCode = "400", description = "Bad Request", content = {
@Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = BadApiRequest.class)
)
})
5. 重構結果
重新運行報告時,我們可以看到我們的變更使錯誤減少了 40% 以上:
讓我們回顧一下我們處理過的一些測試案例。我們現在包含預設的安全標頭:
拒絕格式錯誤的 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.重播測試
在錯誤修復期間,一個有用的命令是replay
,它重新執行特定的測試:
cats replay Test216
我們可以透過查看報告來取得測試編號並在命令中取代它。每個測試的詳細報告還包括完整的replay
命令,以便我們可以將其複製並貼上到我們的終端中。
七、結論
在本文中,我們探討如何使用 CATS 進行自動化 OpenAPI 測試,從而顯著減少手動工作並提高測試覆蓋率。透過應用新增安全標頭、強制輸入驗證和配置嚴格反序列化等更改,我們的範例應用程式報告的錯誤數量減少了 40% 以上。
與往常一樣,原始碼可以在 GitHub 上取得。