Rust 学习笔记:组织测试
Rust 社区将测试分为两大类:单元测试和集成测试。
单元测试较小且更集中,每次只测试一个模块,并且可以测试私有接口。
集成测试完全在库之外,并且以与其他外部代码相同的方式使用代码,仅使用公共接口,并且每个测试可能使用多个模块。
单元测试
单元测试的目的是将每个代码单元与其他代码隔离开来,以快速查明代码在哪里工作,哪里没有按预期工作。
单元测试位于每个文件的 src 目录中,其中包含要测试的代码。惯例是在每个文件中创建一个名为 tests 的模块来包含测试函数,并用 #[cfg(test)] 注释模块。
测试模块和 #[cfg(test)] 注释
回想一下,当我们在本文的第一节中生成新的 adder 项目时,Cargo 为我们生成了以下代码:
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
属性 cfg 代表配置,并告诉 Rust 只有在给定某个配置选项时才应该包含以下项。在本例中,配置选项是 test,它由 Rust 提供,用于编译和运行测试。
#[cfg(test)] 注释告诉 Rust 只在运行 cargo test 时编译和运行测试代码,而不是在运行 cargo build 时。除了带有 #[test] 注释的函数外,这还包括可能在该模块中的任何辅助函数。这样可以在构建库时节省编译时间,并节省编译结果中的空间,因为不包括测试。
测试私有函数
Rust 的隐私规则都允许测试私有函数。测试只是 Rust 代码,而测试模块只是另一个模块。
示例:
pub fn add_two(a: usize) -> usize {
internal_adder(a, 2)
}
fn internal_adder(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
let result = internal_adder(2, 2);
assert_eq!(result, 4);
}
}
注意,internal_adder 函数没有被标记为 pub。因为我们使用 use super::*;
将所有测试模块的父模块的项带入作用域,然后测试可以调用 internal_adder 函数。
集成测试
集成测试完全在库之外,它们以与其他代码相同的方式使用库,这意味着它们只能调用库公共 API 的一部分函数。
集成测试的目的是测试库的许多部分是否可以正确地协同工作。独立工作的代码单元在集成时可能会出现问题,因此集成代码的测试覆盖率也很重要。
要创建集成测试,首先需要一个测试目录。
测试目录
在项目目录 src 旁边创建一个 tests 目录。Cargo 知道在这个目录中查找集成测试文件。然后,我们可以创建任意多的测试文件,Cargo 将把每个文件编译为一个单独的 crate。
再创建一个名为 tests/integration_test.rs 的新文件。当前目录结构应该是这样的:
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
在 tests/integration_test.rs 中编写测试代码:
use adder::add_two;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
Cargo 对 tests 目录进行特殊处理,不需要 #[cfg(test)] 注释,仅在运行 Cargo test 时才编译该目录中的文件。
运行 cargo test:
输出的三个部分包括单元测试、集成测试和文档测试。
请注意,如果某个部分中的任何测试失败,则不会运行后续的部分。
例如,如果单元测试失败,则不会有集成和文档测试的任何输出,因为只有在所有单元测试都通过时才会运行这些测试。
单元测试的第一部分与我们看到的相同:一行用于每个单元测试,然后是单元测试的总结行。
集成测试部分以 Running tests\integration_test.rs 行开头。接下来,在 Doc-tests addder 部分开始之前,有一行用于集成测试中的每个测试功能,还有一行用于集成测试结果的总结行。
每个集成测试文件都有自己的部分,所以如果我们在 tests 目录中添加更多的文件,就会有更多的集成测试部分。
要运行特定集成测试文件中的所有测试,请使用 cargo test 的 --test 参数,后跟文件名:
cargo test --test integration_test
该命令只运行 tests/integration_test.rs 中的所有测试。
集成测试中的子模块
如前所述,tests 目录中的每个文件都被编译为单独的 crate。当有一组辅助函数要在多个集成测试文件中使用时,可以将它们提取到一个公共模块中。
例如,如果我们创建 tests/common.rs 并在其中放置一个名为 setup 的函数,该函数可以在多个测试文件中的多个测试函数中调用。
再次运行测试,我们将在测试输出中看到一个针对 common 的新部分。尽管这个文件不包含任何测试函数,我们也没有从任何地方调用 setup 函数。
在测试结果中显示 common 并显示运行 0 个测试并不是我们想要的,我们只是想与其他集成测试文件共享一些代码。
为了避免在测试输出中出现 common,我们更改为创建 tests/common/mod.rs。项目目录现在看起来像这样:
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
这是 Rust 也理解的旧的命名约定,以这种方式命名文件告诉 Rust 不要将 common 模块视为集成测试文件。我们将 setup 函数代码移动到 tests/common/mod.rs。
测试目录的子目录中的文件不会作为单独的 crate 编译,也不会在测试输出中包含部分。
我们可以在任何集成测试文件中使用 common 模块。下面是一个从 tests/integration_test.rs 中的 it_adds_two 测试中调用 setup 函数的例子:
use adder::add_two;
mod common;
#[test]
fn it_adds_two() {
common::setup();
let result = add_two(2);
assert_eq!(result, 4);
}
二进制 crate 的集成测试
如果我们的项目是一个只包含 src/main.rs 文件而没有 src/lib.rs 文件的二进制 crate,我们就不能在 tests 目录中创建集成测试,也不能使用 use 语句将 src/main.rs 文件中定义的函数引入作用域。
只有库 crate可以暴露函数给其他 crate 使用。二进制 crate 必须独立运行。
这就是提供二进制文件的 Rust 项目有一个直接的 src/main.rs 文件的原因之一,该文件调用 src/lib.rs 文件中的逻辑。使用这种结构,集成测试可以测试库 crate,如果库 crate 提供的功能可以工作,那么 src/main.rs 文件中的少量代码也可以工作,并且这少量代码不需要测试。