6. 第一次集成测试
/health_check 是我们实现的第一个端点。此前,我们使用 curl 手动测试并验证它能够正常工作。
然而,如果项目变大,手动测试会变得低效且容易出错。每次修改代码后都要人工验证,不仅耗时,还可能因为疏忽而遗漏问题。
为了解决这个问题,我们希望尽可能实现自动化测试。这样一来,每次提交代码时,这些检查都能在持续集成(CI)流水线上自动运行,以防止出现回归问题。
这里提到的“回归(Regression)”指的是: 在修改或新增功能后,原本正常的功能因为这次改动而出现故障的情况。自动化测试的目标之一,就是防止功能回归。
虽然健康检查端点的逻辑非常简单,几乎不会变化,但在这里建立起测试脚手架,是构建自动化测试体系的一个理想起点。
被称为"黑盒测试",即通过检查给定的一组输入和输出来测试验证系统的行为,而无需了解内部实现细节。
6.1 如何对端点进行测试
API 是实现特定功能的一种手段,它向外部公开接口,以便执行某种任务(例如存储文档、发送电子邮件等)。
我们在 API 中公开的端点(Endpoint)定义了系统与客户端之间的契约——即对输入和输出的约定。
契约的变更类型
随着时间的推移,API 契约可能会发生变化。一般可以分为两种情况:
-
向后兼容的变更
例如新增一个端点。这类变更不会影响现有客户端的正常使用。 -
破坏性变更
例如删除某个端点,或从响应结构中移除字段。
如果客户端依赖了被删除或修改的部分,那么这些集成可能会因此失效。
为什么要测试端点
虽然我们有时会有意地对 API 契约进行重大调整,但我们必须确保不会无意间破坏现有功能。
为了防止引入用户可见的回归问题,最可靠的方法是模拟用户的实际使用方式,对 API 进行测试。
6.2 黑盒测试的意义
它的核心思想是:只关心输入和输出,而不依赖内部实现细节。这种测试方式通常被称为 黑盒测试(Black-box Testing)。
黑盒测试通过向系统发送一组特定的输入(例如 HTTP 请求),并根据系统返回的输出(响应结果)来验证行为是否符合预期。
它不依赖于内部实现细节,因此可以从用户视角确保整个 API 契约的正确性和稳定性。
遵循这个原则,我们不会满足于直接调用处理器函数的测试,例如
#[cfg(test)]
mod tests {
use crate::health_check;
#[tokio::test]
async fn health_check_succeeds() {
let response = health_check().await;
//这需要将`health_check`函数的返回类型从impl Responder修改为HttpResponse才能编译通过
// 你还需要通过 `use actix_web::HttpResponse` 导入这个类型
assert!(response.status().is_success())
}
}
这种测试方式有几个问题:
-
没有经过 HTTP 层
你没有真正通过 GET 请求调用/health_check,因此无法检查路由是否正确配置。 -
API 契约可能被破坏
如果处理器路径或请求方法发生变化,这类测试仍然可能通过,但实际上 API 已经不符合预期。 -
过于依赖框架实现
Actix-web 的App结构体内部某些参数是私有的,直接在测试中共享初始化逻辑比较困难。
如果应用重构或迁移到其他 Web 框架,现有的测试套件很可能完全失效。
6.3 完全黑盒的解决方案
为了避免上述问题,推荐真正的黑盒测试方法:
- 每次测试启动一个完整的应用程序实例
- 使用 HTTP 客户端(如 reqwest)发送请求
- 通过响应验证 API 的行为
6.4 更改项目结构以便于测试
在/tests下编写真正的测试之前,我们还有一些事要做
/tests下的任何东西最终都会编译成独立的二进制文件–所有测试代码都是以包的形式倒入的。但我们的项目目前是二进制文件,它应该被执行,而不是被共享。因此,不能像这样导入main函数
不妨测试一下
6.4.1 创建一个新的tests文件夹
mkdir -p tests
6.4.2 创建一个新的tests/health_check.rs文件
// tests/health_check.rs
use zero2prod::main; // 错误的
#[test]
fn dummy_test() {
main();
}
构建会失败
我们需要将项目重构为一个库和一个二进制文件;所有逻辑都存在于库中,而二进制文件(例如main.rs)本身是一个入口点,只包含轻量的main函数
6.4.2 首先,需要更改Cargo.toml
[package]
name = "zero2prod"
version = "0.1.0"
edition = "2024"
[lib]
path = "src/lib.rs"
[dependencies]
# ...
我们需要脱离自动配置后,一切简单明了
[package]
name = "zero2prod"
version = "0.1.0"
edition = "2024"
[lib]
path = "src/lib.rs"
[dependencies]
# ...
[[bin]]
path = "src/main.rs"
name = "zero2prod"
lib.rs项目目前还不存在,需要手动创建
6.4.3 将主函数逻辑移入库中
为了让应用程序代码更易于测试,我们将 main 函数中的主要逻辑迁移到库文件 lib.rs 中。
这样可以在测试中直接调用它,而不必依赖可执行文件。
//! main.rs
use zero2prod::run;
#[tokio::main]
async fn main()-> std::io::Result<()> {
run().<

最低0.47元/天 解锁文章
149

被折叠的 条评论
为什么被折叠?



