测试是确保程序正确性的重要手段。虽然测试不能证明程序完全没有 bug,但可以有效地发现 bug 的存在。Rust 设计时高度关注程序的正确性,其类型系统和所有权规则承担了大部分这一负担,但并不能捕获所有问题,因此 Rust 包含了编写自动化测试的支持。
在 Rust 中,测试是标记有特殊属性 #[test]
的函数。测试函数通常执行以下三个操作:
一个基本的测试函数需要添加 #[test]
属性。当运行 cargo test
命令时,Rust 会构建一个测试运行器,运行所有带有此属性的函数,并报告每个测试是否通过。
创建新项目时,Cargo 会自动生成一个包含测试函数的测试模块。
Rust 提供了多种断言宏用于测试:
assert!
- 当表达式为 true
时通过测试 assert!(larger.can_hold(&smaller));
assert_eq!
- 测试两个参数是否相等 assert_eq!(4, add_two(2));
assert_ne!
- 测试两个参数是否不相等 assert_ne!(5, result);
这些宏在测试失败时会提供有用的错误信息。断言宏还可以添加自定义错误消息:
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`",
result
);
#[should_panic]
属性用于测试代码是否按预期发生 panic:
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
为了使 should_panic
测试更精确,可以添加 expected
参数指定 panic 消息中应该包含的文本:
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
测试函数也可以返回 Result<T, E>
,这样可以在测试中使用 ?
运算符:
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
默认情况下,测试是并行运行的。如果测试之间有依赖关系或共享状态,可以使用 --test-threads
参数控制线程数:
$ cargo test -- --test-threads=1
默认情况下,通过的测试中打印到标准输出的内容会被捕获而不显示。如果要查看所有输出,可以使用 --show-output
参数:
$ cargo test -- --show-output
可以通过在 cargo test
命令后指定测试函数名来运行特定测试:
$ cargo test one_hundred
也可以指定测试名称的一部分,所有匹配的测试都会运行:
$ cargo test add // 运行所有名称中含有 "add" 的测试
对于耗时较长的测试,可以使用 #[ignore]
属性标记,使其在一般的测试运行中被跳过:
#[test]
#[ignore]
fn expensive_test() {
// 耗时较长的测试代码
}
要运行被忽略的测试,使用:
$ cargo test -- --ignored
要运行所有测试(包括被忽略的),使用:
$ cargo test -- --include-ignored
Rust 中的测试主要分为两类:单元测试和集成测试。
单元测试的目的是在隔离环境中测试每个代码单元,迅速定位问题所在。单元测试与被测试的代码放在同一个文件中,通常在名为 tests
的模块内,并用 #[cfg(test)]
注解标记。
#[cfg(test)]
注解告诉 Rust 只在运行 cargo test
时编译和运行测试代码,而不是在 cargo build
时,这样可以节省编译时间和编译结果的空间。
Rust 允许测试私有函数。测试模块是普通模块,遵循常规的可见性规则。由于测试模块是内部模块,要测试外部模块的代码需要将其引入作用域:
#[cfg(test)]
mod tests {
use super::*; // 引入父模块的所有内容
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
集成测试完全外部于库,以与其他代码相同的方式使用库,只能调用库的公共 API。它们的目的是测试库的多个部分是否正确地协同工作。
集成测试放在项目根目录下的 tests
目录中。Cargo 会将该目录中的每个文件编译为单独的 crate。
// tests/integration_test.rs
use adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
为了组织多个集成测试,可能希望在 tests
目录创建多个文件。要创建共享的辅助函数而不让它们被视为测试文件,应创建 tests/common/mod.rs
而不是 tests/common.rs
:
tests/
├── common/
│ └── mod.rs // 包含共享函数
└── integration_test.rs
然后在测试中使用:
// tests/integration_test.rs
use adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
对于只有 src/main.rs
而没有 src/lib.rs
的二进制 crate,无法创建直接测试 main.rs
中函数的集成测试。这是 Rust 项目通常将主要逻辑放在 src/lib.rs
中并在 src/main.rs
中调用的原因之一。
Rust 的测试功能提供了一种指定代码应该如何工作的方式,确保在进行更改时代码继续按预期运行。单元测试单独测试库的不同部分,可以测试私有实现细节;集成测试检查库的多个部分是否正确协同工作,并以外部代码使用库的方式测试库的公共 API。
虽然 Rust 的类型系统和所有权规则有助于防止某些类型的错误,但测试对于减少与代码预期行为相关的逻辑错误仍然非常重要。
好好学习,天天向上