测试是一套复杂的规则,不同的人使用不同的术语和组织架构。Rust社区认为测试主要分为两类:单元测试和集成测试。单元测试主要测试规模小更加关注功能,一次只能测试一个模块,而且能够测试私有接口。集成测试对于你的库来说是从外部进行测试并使用代码的方式完全相同于其他外部代码一样,只使用公共接口且可能要跨多个模块。
写这两种方式的测试都是重要的,用于确保你的库符合你的预期,无论是单独的还是一起进行测试。
13.3.1 单元测试
单元测试的目的是测试每一个代码块并精准定位该代码是否符合预期。你将单元测试放入src目录中的每一个文件需要进行测试的代码中。惯例是在每一个文件中创建一个名字叫tests的模块,并使用cfgtest进行声明,其内可以包含测试函数。
13.3.1.1 tests测试模块和#cfg(test)
在tests模块之上的#cfg(test)声明会告诉Rust在运行cargo test命令这时需要编译和运行测试的代码,而使用cargo bulid则不会编译和运行这些代码。这样就会节省大量的时间和空间,因为没有包含测试代码。集成测试在不同的目录,它不需要#cfg(test)声明,但是,因为单元测试和代码在同一个文件,你必须使用#cfg(test)来告诉编译器不要将其编译进编译后的结果。
再次回到生成的第一个库文件时,为我们生成的测试代码:
rust
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);
}
}
在自动生成的tests模块,属性cfg代表配置并且告诉Rust之后的测试项被包含宅配置选项中。在这种情况下,配置项是test,它会由Rust告诉编译器测试时进行编译。通过使用cfg属性,运行cargo test命令只编译我们的测试代码。除了包含声明了#test的函数,在这个模块中还会包含更有用的一些代码。
13.3.1.2 私有功能测试
私有功能是否应该直接进行测试还有争论,而其他编程语言很难或不可能测试私有功能。无论你坚持哪一种理念,Rust允许你测试私有功能。下面的代码演示了如何测试私有功能internal_adder:
rust
pub fn add_two(a: u64) -> u64 {
internal_adder(a, 2)
}
fn internal_adder(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
let result = internal_adder(2, 2);
assert_eq!(result, 4);
}
}
注意internal_adder函数没有标注为pub。子模块中的测试项能够使用祖先模块的中的成员项。在这个测试中,在tests模块中使用use super::*,我们将其所有的成员项带入tests模块,然后就可以调用internal_adder函数了。
13.3.2 集成测试
在Rust中,集成测试对于你的库来说完全是外部的。它们使用你的库和其他代码相同,这意味着你只能调用库中声明为pub的API。它们的目的是测试的库中的各个部分能够协调一致正常工作。在单元测试中能够正确的工作,但是集成在一起不一定会没有问题,因此整合在一起的代码完全测试也非常重要。为了创建集成测试,你需要创建测试目录。
测试目录
我们创建tests目录在项目目录的根目录中,与src毗邻。Cargo知道在这个目录中找到集成测试。我们接着能够在这个目录放入你想要的测试程序文件,Cargo会将这些文件编译为单独的箱(crate)。
让我们创建一个集成测试。首先创建tests目录,然后再该目录中创建一个integration_test.rs文件,这个项目目录结构如下所示:
rust
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
在interation_test.rs文件中写入如下的代码:
rust
use lession13_013::add;
#[test]
fn it_adds_two(){
let result = add(2,2);
assert_eq!(result,4);
}
在tests目录的每一个文件都是一个独立的箱,因此我们需要将需要测试的库带入到每一个测试箱中的作用域。为了这种测试,我们增加lession13_013::add;在代码的顶部,我们不需要单元测试。
我们不需要在integration_test.rs文件中声明#cfg(test)。cargo会特殊对待tests目录并且在使用cargo test命令是会编译这个目录中的文件。下面是运行该命令后的输出信息:
rust
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests\integration_test.rs (target\debug\deps\integration_test-2c4bbd8eb468028f.exe)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests lession13_013
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
输出信息包含三段信息:单元测试、集成测试和文档测试。注意:任何测试类别失败,后面的测试都不会运行。例如:如果单元测试失败,则集成测试和文档测试都不会有任何信息输出,只有在单元测试通过,后面的测试才会执行。
第一段是单元测试,每个单元测试占据一行,最后一行是总述。
第二段是集成测试,列出来文件名和编译后的文件名。其后一行列出集成测试的测试项各占据一行,最后是集成测试的总述。
每个集成测试都有自己的一段,因此在tests目录中可以增加多个文件,输出结果也会显示多个集成测试段。
第三段是文档测试的内容,格式和之前的单元测试一样。
我们也可以指定继承测试的名称来单独执行继承测试。为了对某个继承测试单独执行其内的所有测试项可以使用--test参数,之后加上文件名。如下所示:
rust
cargo test --test integration_test
warning: `C:\Users\xxx\.cargo\config` is deprecated in favor of `config.toml`
|
= help: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.08s
Running tests\integration_test.rs (target\debug\deps\integration_test-2c4bbd8eb468028f.exe)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
指定集成测试也支持文件名的通配符,例如:
bash
cargo test --test integration*
集成测试中的子模块
当你增加了更多的集成测试,你需要在tests目录中增加更多的文件来组织这些测试;例如:你可以将测试按照功能进行分组,每一个文件被编译为一个独立的箱中,对于创建隔离的作用域是非常有用的,以便终端用户可以更好的模拟真实环境那样使用这些箱。但是这些在tests目录中的文件不会像src目录中文件那样会共享相同的行为。
在多个集成测试文件中有一组都会使用的辅助功能,如果要将其抽取放入一个通用的模块中,例如放入到tests/common.rs文件中,在其内写一个setup函数,以便在多个集成测试中调用它:
rust
pub fn setup() {
// setup code specific to your library's tests would go here
}
当运行测试,可以看到信息输出中有common.rs文件,即使它没有包含任何测试代码,也没有调用setup函数,它也会出现在测试输出的信息中。
bash
Running tests\common.rs (target\debug\deps\common-6fd8f73cd685061c.exe)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
如果我们不想要显示这些内容,只想显示需要的内容,可以创建common目录,其内创建mod.rs文件。整个项目的目录如下所示:
bash
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
这样Rust就不会单独编译mod.rs文件,测试结果信息也不会包含不需要的内容。
将之前common文件的内容复制到mod.rs文件中,并修改integration_test文件中的内容如下所示:
rust
use lession13_013::add;
mod common;
#[test]
fn it_adds_two(){
common::setup();
let result = add(2,2);
assert_eq!(result,4);
}
注意mod common的引入和src中的模块引入一样。然后,我们就可以在测试函数中调用common::setup()这个函数了。
为集成测试创建二进制文件
如果我们的项目只包含src中main.rs文件,并不包含src/lib.rs文件,我们就不能在测试目录中创建集成测试,而且也不能使用use语句引入main.rs文件中定义的函数。只有库箱才可以像其他箱暴露可以使用的功能。二进制文件的初衷就意味着独立运作。
这是Rust项目提供一个简洁的main.rs文件的原因之一,该main.rs文件可以调用保存在lib.rs文件中的逻辑。使用这种结构,可以使集成测试调用库箱,从而可以使用其内的重要功能。如果重要功能起作用,则main文件即使少量的代码也可以很好的工作,也就意味着测试只需更少的代码。
13.4 总结
Rust测试特性提供了一种方法来规范代码如何组织才能保证如期所愿,即使修改了代码,测试也可以达到效果。单元测试遍布整个库的不同部分并且可以测试私有实现的细节。集成测试用于验证库代码各个不同部分的协调工作的正确性。一般调用库提供的公共API并如同外部代码那样调用。即使Rust的类型系统和所有权规则阻止了一些漏洞,测试对于减少逻辑错误并确保程序如期动作还是非常重要的。