控制测试如何运行

ch11-02-running-tests.md
commit 1721a106f78c037ca3074d9c2d5a8cf9c9852cf7

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

可以将一部分命令行参数传递给 cargo test,而将另外一部分传递给生成的测试二进制文件。为了分隔这两种参数,需要先列出传递给 cargo test 的参数,接着是分隔符 --,再之后是传递给测试二进制文件的参数。运行 cargo test --help 会提示 cargo test 的有关参数,而运行 cargo test -- --help 可以提示在分隔符 -- 之后使用的有关参数。

并行或连续的运行测试

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

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

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

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

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

显示函数输出

默认情况下,当测试通过时,Rust 的测试库会截获打印到标准输出的所有内容。比如在测试中调用了 println! 而测试通过了,我们将不会在终端看到 println! 的输出:只会看到说明测试通过的提示行。如果测试失败了,则会看到所有标准输出和其他错误信息。

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

文件名: src/lib.rs

  1. fn prints_and_returns_10(a: i32) -> i32 {
  2. println!("I got the value {}", a);
  3. 10
  4. }
  5. #[cfg(test)]
  6. mod tests {
  7. use super::*;
  8. #[test]
  9. fn this_test_will_pass() {
  10. let value = prints_and_returns_10(4);
  11. assert_eq!(10, value);
  12. }
  13. #[test]
  14. fn this_test_will_fail() {
  15. let value = prints_and_returns_10(8);
  16. assert_eq!(5, value);
  17. }
  18. }

示例 11-10:一个调用了 println! 的函数的测试

运行 cargo test 将会看到这些测试的输出:

  1. $ cargo test
  2. Compiling silly-function v0.1.0 (file:///projects/silly-function)
  3. Finished test [unoptimized + debuginfo] target(s) in 0.58s
  4. Running unittests (target/debug/deps/silly_function-160869f38cff9166)
  5. running 2 tests
  6. test tests::this_test_will_fail ... FAILED
  7. test tests::this_test_will_pass ... ok
  8. failures:
  9. ---- tests::this_test_will_fail stdout ----
  10. I got the value 8
  11. thread 'main' panicked at 'assertion failed: `(left == right)`
  12. left: `5`,
  13. right: `10`', src/lib.rs:19:9
  14. note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  15. failures:
  16. tests::this_test_will_fail
  17. test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  18. error: test failed, to rerun pass '--lib'

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

如果你希望也能看到通过的测试中打印的值,也可以在结尾加上 --show-output 告诉 Rust 显示成功测试的输出。

  1. $ cargo test -- --show-output

使用 --show-output 参数再次运行示例 11-10 中的测试会显示如下输出:

  1. $ cargo test -- --show-output
  2. Compiling silly-function v0.1.0 (file:///projects/silly-function)
  3. Finished test [unoptimized + debuginfo] target(s) in 0.60s
  4. Running unittests (target/debug/deps/silly_function-160869f38cff9166)
  5. running 2 tests
  6. test tests::this_test_will_fail ... FAILED
  7. test tests::this_test_will_pass ... ok
  8. successes:
  9. ---- tests::this_test_will_pass stdout ----
  10. I got the value 4
  11. successes:
  12. tests::this_test_will_pass
  13. failures:
  14. ---- tests::this_test_will_fail stdout ----
  15. I got the value 8
  16. thread 'main' panicked at 'assertion failed: `(left == right)`
  17. left: `5`,
  18. right: `10`', src/lib.rs:19:9
  19. note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  20. failures:
  21. tests::this_test_will_fail
  22. test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  23. error: test failed, to rerun pass '--lib'

通过指定名字来运行部分测试

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

为了展示如何运行部分测试,示例 11-11 为 add_two 函数创建了三个测试,我们可以选择具体运行哪一个:

文件名: src/lib.rs

  1. pub fn add_two(a: i32) -> i32 {
  2. a + 2
  3. }
  4. #[cfg(test)]
  5. mod tests {
  6. use super::*;
  7. #[test]
  8. fn add_two_and_two() {
  9. assert_eq!(4, add_two(2));
  10. }
  11. #[test]
  12. fn add_three_and_two() {
  13. assert_eq!(5, add_two(3));
  14. }
  15. #[test]
  16. fn one_hundred() {
  17. assert_eq!(102, add_two(100));
  18. }
  19. }

示例 11-11:不同名称的三个测试

如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行:

  1. $ cargo test
  2. Compiling adder v0.1.0 (file:///projects/adder)
  3. Finished test [unoptimized + debuginfo] target(s) in 0.62s
  4. Running unittests (target/debug/deps/adder-92948b65e88960b4)
  5. running 3 tests
  6. test tests::add_three_and_two ... ok
  7. test tests::add_two_and_two ... ok
  8. test tests::one_hundred ... ok
  9. test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  10. Doc-tests adder
  11. running 0 tests
  12. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

运行单个测试

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

  1. $ cargo test one_hundred
  2. Compiling adder v0.1.0 (file:///projects/adder)
  3. Finished test [unoptimized + debuginfo] target(s) in 0.69s
  4. Running unittests (target/debug/deps/adder-92948b65e88960b4)
  5. running 1 test
  6. test tests::one_hundred ... ok
  7. 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 来运行这两个测试:

  1. $ cargo test add
  2. Compiling adder v0.1.0 (file:///projects/adder)
  3. Finished test [unoptimized + debuginfo] target(s) in 0.61s
  4. Running unittests (target/debug/deps/adder-92948b65e88960b4)
  5. running 2 tests
  6. test tests::add_three_and_two ... ok
  7. test tests::add_two_and_two ... ok
  8. test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

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

忽略某些测试

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

文件名: src/lib.rs

  1. #[test]
  2. fn it_works() {
  3. assert_eq!(2 + 2, 4);
  4. }
  5. #[test]
  6. #[ignore]
  7. fn expensive_test() {
  8. // 需要运行一个小时的代码
  9. }

对于想要排除的测试,我们在 #[test] 之后增加了 #[ignore] 行。现在如果运行测试,就会发现 it_works 运行了,而 expensive_test 没有运行:

  1. $ cargo test
  2. Compiling adder v0.1.0 (file:///projects/adder)
  3. Finished test [unoptimized + debuginfo] target(s) in 0.60s
  4. Running unittests (target/debug/deps/adder-92948b65e88960b4)
  5. running 2 tests
  6. test expensive_test ... ignored
  7. test it_works ... ok
  8. test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
  9. Doc-tests adder
  10. running 0 tests
  11. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

expensive_test 被列为 ignored,如果我们只希望运行被忽略的测试,可以使用 cargo test -- --ignored

  1. $ cargo test -- --ignored
  2. Compiling adder v0.1.0 (file:///projects/adder)
  3. Finished test [unoptimized + debuginfo] target(s) in 0.61s
  4. Running unittests (target/debug/deps/adder-92948b65e88960b4)
  5. running 1 test
  6. test expensive_test ... ok
  7. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
  8. Doc-tests adder
  9. running 0 tests
  10. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

通过控制运行哪些测试,你可以确保能够快速地运行 cargo test 。当你需要运行 ignored 的测试时,可以执行 cargo test -- --ignored。如果你希望不管是否忽略都要运行全部测试,可以运行 cargo test -- --include-ignored