我们来讨论一下怎样测试Rust代码。我们不会讨论的是正确的方式测试Rust代码。有很多学校里学的思想是使用正确和错误的方式写测试。所有的这些方法是用类似的基本工具,我们将会给你展示使用它们的语法。
test属性
最简单的,在Rust中的测试是一个拥有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,将会产生错误。让我们再次运行test:
$ 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);
}
如果发生错误测试将会成功并且如果没有错误会失败,让我们尝试一下吧:
$ 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测试是脆弱的,因为它很难保证测试不会因为一个非预期的原因导致失败。为了解决这个问题,一个可选的expected参数可以加在should_panic属性之上。一个安全版本的例子是:
#[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!的一个很普通用法:使用已知的参数调用函数并将其与期望的输出比较。
test模块
我们上面的例子不是习惯用法:它没有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属性说明tests模块。这个模块允许我们将所有的测试跟组,如果有需要也可以定义帮助函数,它将不会成为我们的crate的一部分。如果我们试图运行测试用例,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 tests::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模块来进行单元测试。在这里仅仅测试小功能是有意义的。但是集成测试怎么办?对这个来说我们有tests目录。
tests目录
系一个集成测试用例,让我们创建一个tests目录,并把tests/lib.rs文件放进去,下面是该文件的内容:
extern crate adder;
#[test]
fn it_works() {
assert_eq!(4, adder::add_two(2));
}
这个看起来跟前面的测试用例类似,但是稍微有些不同。我们现在顶部有一个extern crate adder。这是因为在tests目录下的测试用例是完全分开的crate,因此我们需要导入我们的库。这就是为什么tests是一个合适的地方来写继承测试用例:他们像其他用户一样使用库。
让我们运行它:
$ cargo test
Compiling adder v0.0.1 (file:///home/you/projects/adder)
Running target/adder-91b3e234d4ed382a
running 1 test
test tests::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目录中的所有东西。tests模块在这里并不需要,因为整个狮子那个都聚焦在测试用例上。
让我们查看一下第三部分:文档测试。
文档测试
没有什么比带有例子的文档更好了。没有什么比不能正常工作的例子更糟糕,不能工作的原因是文档被写完后代码做了修改。鉴于此,Rust支持西贡运行文档里的例子。这是一个含有例子的src/lib.rs:
//! The `adder` crate provides functions that add numbers to other numbers.
//!
//! # Examples
//!
//! ```
//! assert_eq!(4, adder::add_two(2));
//! ```
/// This function adds two to its argument.
///
/// # Examples
///
/// ```
/// use adder::add_two;
///
/// assert_eq!(4, add_two(2));
/// ```
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,三个‘`’标记代码块。按惯例包含# Example节,详见下面的例子。
让我们再次运行测试用例:
$ cargo test
Compiling adder v0.0.1 (file:///home/steve/tmp/adder)
Running target/adder-91b3e234d4ed382a
running 1 test
test tests::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
现在我们已经能使三种测试用例运行!注意文档测试的名字:_0是为模块测试产生的名字,add_two_0是为函数测试产生的名字。当你添加更多例子时这些将会自动增加,比如add_two_1。