4.2.测试

最后更新于:2022-04-01 00:43:04

> Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence. > > Edsger W. Dijkstra, "The Humble Programmer" (1972) > > 软件测试是证明bug存在的有效方法,而证明它们不存在时则显得令人绝望的不足。 > > Edsger W. Dijkstra,【谦卑的程序员】(1972) 让我们讨论一下如何测试Rust代码。在这里我们不会讨论什么是测试Rust代码的正确方法。这里有很多关于写测试好坏方法的流派。所有的这些途径都使用相同的基本工具,所以我们会想你展示他们的语法。 ## `测试`属性(The test attribute) 简单的说,测试是一个标记为`test`属性的函数。让我们用Cargo来创建一个叫`adder`的项目: ~~~ $ cargo new adder $ cd adder ~~~ 在你创建一个新项目时Cargo会自动生成一个简单的测试。下面是`src/lib.rs`的内容: ~~~ #[test] fn it_works() { } ~~~ 注意这个`#[test]`。这个属性表明这是一个测试函数。它现在没有函数体。它肯定能编译通过!让我们用`cargo test`运行测试: ~~~ $ cargo test Compiling adder v0.0.1 (file:///home/you/projects/adder) Running target/adder-91b3e234d4ed382a running 1 test test it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ~~~ Cargo编译和运行了我们的测试。这里有两部分输出:一个是我们写的测试,另一个是文档测试。我们稍后再讨论这些。现在,看看这行: ~~~ test it_works ... ok ~~~ 注意那个`it_works`。这是我们函数的名字: ~~~ fn it_works() { ~~~ 然后我们有一个总结行: ~~~ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured ~~~ 那么为啥我们这个啥都没干的测试通过了呢?任何没有`panic!`的测试通过,`panic!`的测试失败。让我们的测试失败: ~~~ #[test] fn it_works() { assert!(false); } ~~~ `assert!`是Rust提供的一个宏,它接受一个参数:如果参数是`true`,啥也不会发生。如果参数是`false`,它会`panic!`。让我们再次运行我们的测试: ~~~ $ cargo test Compiling adder v0.0.1 (file:///home/you/projects/adder) Running target/adder-91b3e234d4ed382a running 1 test test it_works ... FAILED failures: ---- it_works stdout ---- thread 'it_works' panicked at 'assertion failed: false', /home/steve/tmp/adder/src/lib.rs:3 failures: it_works test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured thread '<main>' panicked at 'Some tests failed', /home/steve/src/rust/src/libtest/lib.rs:247 ~~~ Rust指出我们的测试失败了: ~~~ test it_works ... FAILED ~~~ 这反映在了总结行上: ~~~ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured ~~~ 我们也得到了一个非0的状态值: ~~~ $ echo $? 101 ~~~ 这在你想把`cargo test`集成进其它工具是非常有用。 我们可以使用另一个属性反转我们的失败的测试:`should_panic`: ~~~ #[test] #[should_panic] fn it_works() { assert!(false); } ~~~ 现在即使我们`panic!`了测试也会通过,并且如果我们的测试通过了则会失败。让我试一下: ~~~ $ cargo test Compiling adder v0.0.1 (file:///home/you/projects/adder) Running target/adder-91b3e234d4ed382a running 1 test test it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ~~~ Rust提供了另一个宏,`assert_eq!`用来比较两个参数: ~~~ #[test] #[should_panic] fn it_works() { assert_eq!("Hello", "world"); } ~~~ 那个测试通过了吗?因为那个`should_panic`属性,它通过了: ~~~ $ cargo test Compiling adder v0.0.1 (file:///home/you/projects/adder) Running target/adder-91b3e234d4ed382a running 1 test test it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ~~~ `should_panic`测试是脆弱的,因为很难保证测试是否会因什么不可预测原因并未失败。为了解决这个问题,`should_panic`属性可以添加一个可选的`expected`参数。这个参数可以确保失败信息中包含我们提供的文字。下面是我们例子的一个更安全的版本: ~~~ #[test] #[should_panic(expected = "assertion failed")] fn it_works() { assert_eq!("Hello", "world"); } ~~~ 这就是全部的基础内容!让我们写一个“真实”的测试: ~~~ pub fn add_two(a: i32) -> i32 { a + 2 } #[test] fn it_works() { assert_eq!(4, add_two(2)); } ~~~ `assert_eq!`是非常常见的;用已知的参数调用一些函数然后与期望的输出进行比较。 # `tests`模块 这有一个问题令我们当前的项目并不理想:它缺少`tests`模块。我们例子的理想写法如下: ~~~ pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::add_two; #[test] fn it_works() { assert_eq!(4, add_two(2)); } } ~~~ 这里产生了一些变化。第一个变化是引入了一个`cfg`属性的`mod tests`。这个模块允许我们把所有测试集中到一起,并且需要的话还可以定义辅助函数,它们不会成为我们包装箱的一部分。`cfg`属性只会在我们尝试去运行测试时才会编译测试代码。这样可以节省编译时间,并且也确保我们的测试代码完全不会出现在我们的正式构建中。 第二个变化是`use`声明。因为我们在一个内部模块中,我们需要把我们要测试的函数导入到当前空间中。如果你有一个大型模块的话这会非常烦人,所以这里有经常使用一个`glob`功能。让我们修改我们的`src/lib.rs`来使用这个: ~~~ pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { assert_eq!(4, add_two(2)); } } ~~~ 注意`use`行的变化。现在运行我们的测试: ~~~ $ cargo test Updating registry `https://github.com/rust-lang/crates.io-index` Compiling adder v0.0.1 (file:///home/you/projects/adder) Running target/adder-91b3e234d4ed382a running 1 test test test::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ~~~ 目前的习惯是使用`test`模块来存放你的“单元测试”。任何只是测试一小部分功能的测试理应放在这里。那么“集成测试”怎么办呢?我们有`tests`目录来处理这些。 ## `tests`目录 为了进行集成测试,让我们创建一个`tests`目录,然后放一个`tests/lib.rs`文件进去,输入如下内容: ~~~ extern crate adder; #[test] fn it_works() { assert_eq!(4, adder::add_two(2)); } ~~~ 这看起来与我们刚才的测试很像,不过有些许的不同。我们现在有一行`extern crate adder`在开头。这是因为在`tests`目录中的测试另一个完全不同的包装箱,所以我们需要导入我们的库。这也是为什么`tests`是一个写集成测试的好地方:它们就想其它程序一样使用我们的库。 让我们运行一下: ~~~ $ cargo test Compiling adder v0.0.1 (file:///home/you/projects/adder) Running target/adder-91b3e234d4ed382a running 1 test test test::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured Running target/lib-c18e7d3494509e74 running 1 test test it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ~~~ 现在我们有了三个部分:我们之前的两个测试,然后还有我们新添加的。 这就是`tests`目录的全部内容。它不需要`test`模块因为它整个就是关于测试的。 让我们最后看看第三部分:文档测试。 ## 文档测试 没有什么是比带有例子的文档更好的了。当然也没有什么比不能工作的例子更糟的,因为文档完成之后代码已经被改写。为此,Rust支持自动运行你文档中的例子。这是一个完整的有例子的`src/lib.rs`: ~~~ //! The `adder` crate provides functions that add numbers to other numbers. //! //! # Examples //! //! ` ` ` 因为gitbook排版问题,这里多写了两个空格 //! assert_eq!(4, adder::add_two(2)); //! ` ` ` 因为gitbook排版问题,这里多写了两个空格 /// This function adds two to its argument. /// /// # Examples /// /// ` ` ` 因为gitbook排版问题,这里多写了两个空格 /// use adder::add_two; /// /// assert_eq!(4, add_two(2)); /// ` ` ` 因为gitbook排版问题,这里多写了两个空格 pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { assert_eq!(4, add_two(2)); } } ~~~ 注意模块级的文档以`//!`开头然后函数级的文档以`///`开头。Rust文档在注释中支持Markdown语法,所以它支持3个反单引号代码块语法。想上面例子那样,加入一个`# Examples`部分被认为是一个惯例。 让我们再次运行测试: ~~~ $ cargo test Compiling adder v0.0.1 (file:///home/steve/tmp/adder) Running target/adder-91b3e234d4ed382a running 1 test test test::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured Running target/lib-c18e7d3494509e74 running 1 test test it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured Doc-tests adder running 2 tests test add_two_0 ... ok test _0 ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured ~~~ 现在我们运行了3种测试!注意文档测试的名称:`_0`生成为模块测试,而`add_two_0`函数测试。如果你添加更多用例的话它们会像`add_two_1`这样自动加一。
';