Rust-测试

一、如何编写测试

测试用来验证非测试的代码是否按照期望的方式运行的 Rust 函数。测试函数体通常执行如下 三种操作:

    1. 设置任何所需的数据或状态
    1. 运行需要测试的代码
    1. 断言其结果是我们所期望的

让我们看看 Rust 提供的专门用来编写测试的功能: test 属性、一些宏和 should_panic 属 性。

测试函数剖析

作为最简单例子,Rust 中的测试就是一个带有 test 属性注解的函数。属性(attribute)是 关于 Rust 代码片段的元数据。
为了将 一个函数变成测试函数,需要在 fn 行之前加上 #[test] 。当使用 cargo test 命令运行测 试函数时,Rust 会构建一个测试执行者二进制文件用来运行标记了 test 属性的函数并报告 每一个测试是通过还是失败。

$ cargo new adder --lib
Created library `adder` project
$ cd adder

首先创建一个lib。

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

现在让我们暂时忽略 tests 模块和 #[cfg(test)] 注解并只关注函数来了解其如何工作。注 意 fn 行之前的 #[test] :这个属性表明这是一个测试函数,这样测试执行者就知道将其作 为测试处理。因为也可以在 tests 模块中拥有非测试的函数来帮助我们建立通用场景或进行 常见操作,所以需要使用 #[test] 属性标明哪些函数是测试。

函数体使用 assert_eq! 宏断言 2 加 2 等于 4。这个断言作为一个典型测试格式的例子。让 我们运行以便看到测试通过。
cargo test 命令会运行项目中所有的测试:

$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.57s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

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

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cargo 编译并运行了测试。在 Compiling 、 Finished 和 Running 这几行之后,可以看到 running 1 test 这一行。下一行显示了生成的测试函数的名称,它是 it_works ,以及测试 的运行结果, ok 。接着可以看到全体测试运行结果的总结: test result: ok. 意味着所有测试都通过了。 1 passed; 0 failed 表示通过或失败的测试数量。

这里并没有任何被标记为忽略的测试,所以总结表明 0 ignored 。我们也没有过滤需要运行 的测试,所以总结的结尾显示 0 filtered out 。在下一部分 “控制测试如何运行” 会讨论忽略 和过滤测试。

0 measured 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能 用于 Rust 开发版(nightly Rust)。

测试输出中以 Doc-tests adder 开头的这一部分是所有文档测试的结果。现在并没有任何文 档测试,不过 Rust 会编译任何出现在 API 文档中的代码示例。这个功能帮助我们使文档和代 码保持同步!

让我们改变测试的名称并看看这如何改变测试的输出。给 it_works 函数起个不同的名字, 比如 exploration ,像这样:

#[cfg(test)]
mod tests {
#[test]
fn exploration() {
assert_eq!(2 + 2, 4);
}
}

并再次运行 cargo test 。现在输出中将出现 exploration 而不是 it_works :
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.59s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::exploration ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试 就失败了。每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测 试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用 panic! 宏。

#[cfg(test)]
mod tests {
#[test]
fn exploration() {
assert_eq!(2 + 2, 4);
}

#[test]
fn another() {
panic!("Make this test fail");
}
}

再次 cargo test 运行测试。输出应该看起来如下所示,它表明 exploration 测试通过了而 another 失败了:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.72s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

test tests::another 这一行是 FAILED 而不是 ok 了。在单独测试结果和总结之间多了两 个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中, another 因为panicked at ‘Make this test fail’ 而失败,这位于 src/lib.rs 的第 10 行。下一部分仅仅列 出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助。可以使用失 败测试的名称来只运行这个测试,这样比较方便调试;下一部分 “控制测试如何运行” 会讲到 更多运行测试的方法。

最后是总结行:总体上讲,测试结果是 FAILED 。有一个测试通过和一个测试失败。

现在我们见过不同场景中测试结果是什么样子的了,再来看看除 panic! 之外的一些在测试 中有帮助的宏吧。

使用 assert! 宏来检查结果

assert! 宏由标准库提供,在希望确保测试中一些条件为 true 时非常有用。需要向assert! 宏提供一个计算为布尔值的参数。如果值是 true , assert! 什么也不做同时测试 会通过。如果值为 false , assert! 调用 panic! 宏,这会导致测试失败。 assert! 宏帮 助我们检查代码是否以期望的方式运行。

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

can_hold 方法返回一个布尔值,这意味着它完美符合 assert! 宏的使用场景。上面代码中,让我们编写一个 can_hold 方法的测试来作为练习,这里创建一个长为 8 宽为 7 的Rectangle 实例,并假设它可以放得下另一个长为 5 宽为 1 的 Rectangle 实例:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};

assert!(larger.can_hold(&smaller));
}
}

注意在 tests 模块中新增加了一行: use super::*; 。 tests 是一个普通的模块,它遵循 “私有性规则” 部分介绍的常用可见性规则。因为这是一个内部模块,需要将外部模块中 被测试的代码引入到内部模块的作用域中。这里选择使用全局导入使得外部模块定义的所有 内容在 tests 模块中都是可用的。

我们将测试命名为 larger_can_hold_smaller ,并创建所需的两个 Rectangle 实例。接着调 用 assert! 宏并传递 larger.can_hold(&smaller) 调用的结果作为参数。这个表达式预期会 返回 true ,所以测试应该通过。让我们拭目以待!

$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests (target/debug/deps/rectangle-6584c4561e48942e)

running 1 test
test tests::larger_can_hold_smaller ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

它确实通过了!

使用 assert_eq! 和 assert_ne! 宏来测试相等

可以通 过向 assert! 宏传递一个使用 == 运算符的表达式来做到。不过这个操作实在是太常见 了,以至于标注库提供了一对宏来更方便的处理这些操作: assert_eq! 和 assert_ne! 。这 两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什 么,以便于观察测试 为什么 失败,而 assert! 只会打印出它从 == 表达式中得到了 false 值,而不是导致 false 的两个值。

pub fn add_two(a: i32) -> i32 {
a + 2
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
}

让我们检查它是否通过!
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

传递给 assert_eq! 宏的第一个参数,4,等于调用 add_two(2) 的结果。我们将会看到这个 测试的那一行说 test tests::it_adds_two … ok , ok 表明测试通过了!
注意在一些语言和测试框架中,断言两个值相等的函数的参数叫做 expected 和 actual ,而 且指定参数的顺序是需要注意的。然而在 Rust 中,他们则叫做 left 和 right ,同时指定 期望的值和被测试代码产生的值的顺序并不重要。

assert_ne! 宏在传递给它的两个值不相等时通过而在相等时失败。

assert_eq! 和 assert_ne! 宏在底层分别使用了 == 和 != 。当断言失败时,这些宏会使 用调试格式打印出其参数,这意味着被比较的值必需实现了 PartialEq 和 Debug trait。所有 的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现PartialEq 才能断言他们的值是否相等。需要实现 Debug 才能在断言失败时打印他们的值。 因为这两个 trait 都是派生 trait,通常可以直接在结构体或枚举 上添加 #[derive(PartialEq, Debug)] 注解。

自定义错误信息

也可以向 assert! 、 assert_eq! 和 assert_ne! 宏传递一个可选的参数来增加用于打印的自 定义错误信息。任何在 assert! 必需的一个参数和 assert_eq! 和 assert_ne! 必需的两个 参数之后指定的参数都会传递给format! 宏,所以可以传递一个包含 {} 占位符的格式字符串和放入占位符的值。自定义信息有助于记录断言的意义,这样到测试失败 时,就能更好的理解代码出了什么问题。

pub fn greeting(name: &str) -> String {
format!("Hello {}!", name)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}

这个程序的需求还没有被确定,而我们非常确定问候开始的 Hello 文本不会改变。我们决定 并不想在人名改变时不得不更新测试,所以相比检查 greeting 函数返回的确切的值,我们 将仅仅断言输出的文本中包含输入参数。
让我们通过将 greeting 改为不包含 name 来在代码中引入一个 bug 来测试失败时是怎样 的,
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}

运行此测试会产生以下结果:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.91s
Running unittests (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'assertion failed: result.contains(\"Carol\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

仅仅告诉了我们断言失败了和失败的行号。一个更有用的错误信息应该打印出从 greeting 函数得到的值。让我们改变测试函数来使用一个由包含占位符的格式字符串和从 greeting 函数取得的值组成的自定义错误信息:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`",
result
);
}
}

现在,当我们运行测试时,我们将收到一条信息更丰富的错误消息:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.93s
Running unittests (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

可以在测试输出中看到所取得的确切的值,这会帮助我们理解真正发生了什么而不是期望发 生什么。

使用 should_panic 检查 panic

除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重 要的。
可以通过对函数增加另一个属性 should_panic 来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。

pub struct Guess {
value: i32,
}

impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}

Guess { value }
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}

#[should_panic] 属性位于 #[test] 之后和对应的测试函数之前。让我们看看测试通过时它是什么样子:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test

看起来不错!现在在代码中引入 bug,移除 new 函数在值大于 100 时会 panic 的条件:
pub struct Guess {
value: i32,
}

// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}

Guess { value }
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}

它会失败:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.62s
Running unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----
note: test did not panic as expected

failures:
tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了 #[should_panic] 。这个错误意味着代码中函数 Guess::new(200) 并没有产生 panic。

然而 should_panic 测试可能是非常含糊不清的,因为他们只是告诉我们代码并没有产生 panic。 should_panic 甚至在测试因为其他不同的原因而不是我们期望发生的情况而 panic 时 也会通过。为了使 should_panic 测试更精确,可以给 should_panic 属性增加一个可选的expected 参数。测试工具会确保错误信息中包含其提供的文本。

这个测试会通过,因为 should_panic 属性中 expected 参数提供的值是 Guess::new 函数 panic 信息的子字符串。我们可以指定期望的整个 panic 信息,在这个例子中是 Guess value must be less than or equal to 100, got 200. 。这依赖于 panic 有多独特或动态,和你希望测 试有多准确。在这个例子中,错误信息的子字符串足以确保函数在 else if value > 100 的 情况下运行。

pub struct Guess {
value: i32,
}

impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be less than or equal to 100, got {}.",
value
);
} else if value > 100 {
panic!(
"Guess value must be greater than or equal to 1, got {}.",
value
);
}

Guess { value }
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}

这一次运行 should_panic 测试,它会失败:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----
thread 'main' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: `"Guess value must be greater than or equal to 1, got 200."`,
expected substring: `"Guess value must be less than or equal to 100"`

failures:
tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

错误信息表明测试确实如期望 panic 了,不过 panic 信息是 did not include expected string ‘Guess value must be less than or equal to 100’ 。可以看到我们得到的 panic 信息,在这个 例子中是 Guess value must be greater than or equal to 1, got 200. 。这样就可以开始寻找 bug 在哪了!

Result在测试中使用

到目前为止,我们已经编写了在失败时panic!的测试。我们还可以编写使用Result<T, E>

#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}

该it_works函数现在有一个返回类型,Result<(), String>。在函数体中没有assert_eq!,我们没有调用宏,而是Ok(())在测试通过时返回,在测试失败时返回 一个Errwith String。

编写测试使其返回一个Result使您能够在测试主体中使用问号运算符,这是编写测试的便捷方法,如果其中的任何操作返回Err变体,则该测试应该失败。

您不能#[should_panic]在使用Result. 相反,您应该Err在测试失败时直接返回一个值。

二、控制测试如何运行

就像 cargo run 会编译代码并运行生成的二进制文件一样, cargo test 在测试模式下编译 代码并运行生成的测试二进制文件。可以指定命令行参数来改变 cargo test 的默认行为。
例如, cargo test 生成的二进制文件的默认行为是并行的运行所有测试,并捕获测试运行过 程中产生的输出避免他们被显示出来,使得阅读测试结果相关的内容变得更容易。

这些选项的一部分可以传递给 cargo test ,而另一些则需要传递给生成的测试二进制文件。 为了分隔两种类型的参数,首先列出传递给 cargo test 的参数,接着是分隔符 — ,再之 后是传递给测试二进制文件的参数。运行 cargo test —help 会告诉你 cargo test 的相关 参数,而运行 cargo test — —help 则会告诉你位于分隔符 — 之后的相关参数。

并行或连续的运行测试

当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所 以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相 互依赖或依赖任何共享状态,这包括类似于当前工作目录或者环境变量这样的共享环境。

例如,每一个测试都运行一些代码在硬盘上创建一个 test-output.txt 文件并写入一些数 据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测 试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过 程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运 行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测 试。

如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递 —test- threads 参数和希望使用线程的数量给测试二进制文件。例如:

$ cargo test -- --test-threads=1

这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时 间,不过测试就不会在存在共享状态时潜在的相互干涉了。

显示函数输出

如果测试通过了,Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试 中调用 println! 而测试通过了,我们将不会在终端看到 println! 的输出:只会看到说明 测试通过的行。如果测试失败了,就会看到所有标准输出和其他错误信息。

有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会 通过的测试和一个会失败的测试:

fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {}", a);
10
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(10, value);
}

#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(5, value);
}
}

当我们使用 运行这些测试时cargo test,我们将看到以下输出:
$ cargo test
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `10`', src/lib.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

注意输出中哪里也不会出现 I got the value 4 ,这是当测试通过时打印的内容。这些输出被 捕获。失败测试的输出, I got the value 8 ,则出现在输出的测试总结部分,同时也显示了 测试失败的原因。

如果你希望也能看到通过的测试中打印的值,捕获输出的行为可以通过 —nocapture 参数来 禁用:

$ cargo test -- --show-output

当我们再次使用—show-output标记运行示例 11-10 中的测试时,我们看到以下输出:
$ cargo test -- --show-output
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished test [unoptimized + debuginfo] target(s) in 0.60s
Running unittests (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

successes:

---- tests::this_test_will_pass stdout ----
I got the value 4


successes:
tests::this_test_will_pass

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `10`', src/lib.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

通过名称来运行测试的子集

有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行这 些代码相关的测试。可以向 cargo test 传递希望运行的测试的(部分)名称作为参数来选 择运行哪些测试。

为了演示如何运行测试的子集,我们将为我们的add_two函数创建三个测试 ,

pub fn add_two(a: i32) -> i32 {
a + 2
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn add_two_and_two() {
assert_eq!(4, add_two(2));
}

#[test]
fn add_three_and_two() {
assert_eq!(5, add_two(3));
}

#[test]
fn one_hundred() {
assert_eq!(102, add_two(100));
}
}

如果我们在不传递任何参数的情况下运行测试,正如我们之前看到的,所有测试将并行运行:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.62s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

运行单个测试

可以向 cargo test 传递任意测试的名称来只运行这个测试:

$ cargo test one_hundred
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.69s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::one_hundred ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

只有名称为 one_hundred 的测试被运行了;其余两个测试并不匹配这个名称。测试输出在总 结行的结尾显示了 2 filtered out 表明存在比本命令所运行的更多的测试。
不能像这样指定多个测试名称,只有传递给 cargo test 的第一个值才会被使用。不过有运 行多个测试的方法。

过滤运行多个测试

然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含 add ,可以通过 cargo test add 来运行这两个测试:

$ cargo test add
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

这运行了所有名字中带有 add 的测试。同时注意测试所在的模块作为测试名称的一部分,所 以可以通过模块名来过滤运行一个模块中的所有测试。

除非指定否则忽略某些测试

有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 cargo test 的时候希 望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用 ignore 属性来标记 耗时的测试并排除他们,如下所示:

#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}

对想要排除的测试的 #[test] 之后增加了 #[ignore] 行。现在如果运行测试,就会发现 it_works 运行了,而 expensive_test 没有运行:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.60s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test expensive_test ... ignored
test it_works ... ok

test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

expensive_test 被列为 ignored ,如果只希望运行被忽略的测试,可以使用 cargo test — —ignored :
$ cargo test -- --ignored
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test expensive_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

通过控制运行哪些测试,可以确保运行 cargo test 的结果是快速的。当某个时刻需要检查 ignored 测试的结果而且你也有时间等待这个结果的话,可以选择执行 cargo test — —ignored 。

三、测试的组织结构

正如之前提到的,测试是一个复杂的概念,而且不同的开发者也采用不同的技术和组织。 Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试(unit tests)与 集成测试 (integration tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块, 也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户采用相同 的方式使用你的代码,他们只针对公有接口而且每个测试都会测试多个模块。

编写这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。

单元测试

单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的 定位代码位于何处和是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相 同的文件中。传统做法是在每个文件中创建包含测试函数的 tests 模块,并使用 cfg(test) 标注模块。

测试模块和 cfg(test)

测试模块的 #[cfg(test)] 注解告诉 Rust 只在执行 cargo test 时才编译和运行测试代码, 而在运行 cargo build 时不这么做。这在只希望构建库的时候可以节省编译时间,并能节省 编译产物的空间因为他们并没有包含测试。我们将会看到因为集成测试位于另一个文件夹, 他们并不需要 #[cfg(test)] 注解。但是因为单元测试位于与源码相同的文件中,所以使用#[cfg(test)] 来指定他们不应该被包含进编译结果中。

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

这里自动生成了测试模块。 cfg 属性代表 configuration ,它告诉 Rust 其之后的项只被包含 进特定配置中。在这个例子中,配置是 test ,Rust 所提供的用于编译和运行测试的配置。 通过使用这个属性,Cargo 只会在我们主动使用 cargo test 运行测试时才编译测试代码。 除了标注为 #[test] 的函数之外,还包括测试模块中可能存在的帮助函数。

测试私有函数

测试社区中一直存在关于是否应该对私有函数进行单元测试的论战,而其他语言中难以甚至 不可能测试私有函数。不过无论你坚持哪种测试意识形态,Rust 的私有性规则确实允许你测 试私有函数,由于私有性规则。

pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}

注意 internal_adder 函数并没有标记为 pub ,不过因为测试也不过是 Rust 代码同时tests 也仅仅是另一个模块,我们完全可以在测试中导入和调用 internal_adder 。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。

集成测试

在 Rust 中,集成测试对于需要测试的库来完全说是外部的。他们同其他代码一样使用库文 件,这意味着他们只能调用作为库公有 API 的一部分函数。他们的目的是测试库的多个部分 能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集 成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 tests 目录。

tests 目录
为了编写集成测试,需要在项目根目录创建一个 tests 目录,与 src 同级。Cargo 知道如何去 寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。
让我们来创建一个集成测试!
tests/integration_test.rs

use adder;

#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}

我们在顶部增加了 use adder ,这在单元测试中是不需要的。这是因为每一个tests 目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。这里的adder 是项目名称。

并不需要将 tests/integration_test.rs 中的任何代码标注为 #[cfg(test)] 。Cargo 对 tests 文件夹特殊处理并只会在运行 cargo test 时编译这个目录中的文件。现在就运行 cargo test 试试:

$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 1.31s
Running unittests (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::internal ... 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-1082c4b063a8fbe6)

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 adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

现在有了三个部分的输出:单元测试、集成测试和文档测试。

我们仍然可以通过指定测试函数的名称作为 cargo test 的参数来运行特定集成测试。为了 运行某个特定集成测试文件中的所有测试,使用 cargo test 的 —test 后跟文件的名称:

$ cargo test --test integration_test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.64s
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

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

此命令仅运行tests/integration_test.rs文件中的测试。

集成测试中的子模块

随着集成测试的增加,你可能希望在 tests 目录增加更多文件辅助组织他们,例如根据测试 的功能来将测试分组。正如我们之前提到的,每一个 tests 目录中的文件都被编译为单独的 crate。
将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样 的单独的作用域。

tests/common.rs

pub fn setup() {
// setup code specific to your library's tests would go here
}

如果再次运行测试,将会在测试结果中看到一个对应 common.rs 文件的新部分,即便这个文 件并没有包含任何测试函数,或者没有任何地方调用了 setup 函数:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.89s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

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 adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

common 出现在测试结果中并显示 running 0 tests ,这不是我们想要的;我们只是希望能够 在其他集成测试文件中分享一些代码罢了。

为了避免 common 出现在测试输出中,不同于创建 tests/common.rs,我们将创建 tests/common/mod.rs。以这种方式命名文件告诉 Rust 不要将common模块视为集成测试文件。当我们将setup函数代码移动到tests/common/mod.rs中并删除 tests/common.rs文件时,测试输出中的部分将不再出现。测试目录子目录中的文件不会被编译为单独的板条箱,也不会在测试输出中包含部分。

一旦拥有了 tests/common/mod.rs,就可以将其作为模块来在任何集成测试文件中使用。这里 是一个 tests/integration_test.rs 中调用 setup 函数的 it_adds_two 测试的例子:
tests/integration_test.rs

use adder;

mod common;

#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}

二进制 crate 的集成测试

如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 创建 集成测试并使用 use 导入 src/main.rs 中的函数了。只有库 crate 向其他 crate 暴露 了可供调用和使用的函数;二进制 crate 只意在单独运行。
这也是 Rust 二进制项目明确采用 src/main.rs 调用 src/lib.rs 中逻辑这样的结构的原因之一。 通过这种结构,集成测试 就可以 使用 use 测试库 crate 中的主要功能了,而如果 这些重要的功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。