Rust 3.3 测试
17 Jan 2015 by LelouchHe
接下来学习如何测试Rust代码.我们不会讨论测试的正确方法.关于测试的正确/错误方法是有很多不同看法的.然而,这些方法都要使用相同的工具,这里我们学习下这些工具的用法.
test属性
最简单的情况,Rust中的测试就是一个带有test
属性(attribute)的函数.新建一个adder
的项目:
$ cargo new adder
$ cd adder
Cargo在新建项目时,会自动生成一个简单的测试代码.如下就是src/lib.rs
:
#[test]
fn it_works() {
}
注意#[test]
.这个属性表示这是个测试函数.现在还没有任何内容.所以肯定是能测试通过的.可以用cargo test
来运行测试:
$ cargo test
Cargo编译并运行测试代码.输出有2种: 一种是我们写的测试,里搞一个是文档测试(documentation test).后者我们后面详述.现在,看这行:
test it_works ... ok
注意it_works
.这就是测试函数的名称:
fn it_works() {
}
我们还会得到一行总结:
test result: ok. 1 passed; 0 failed; 0 ignored; 0 messured
为什么没有内容的测试能通过呢?测试代码里,panic!
会导致测试失败,如果没有panic!
,就表示测试成功.如下就会测试失败:
#[test]
fn it_works() {
assert!(false);
}
assert!
是Rust提供的宏,带一个参数:这个参数为真时,没有任何影响,否则,就会调用panic!
.再运行下测试:
$ cargo test
Rust指出测试失败了:
test it_works ... FAILED
总结行里也有:
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 messured
同样还有非0的返回值:
$ echo $?
101
当你需要将cargo test
集成到其他工具中去的话,返回值是很有用的.
接下来使用should_fail
属性,反转测试失败的情况:
#[test]
#[should_fail]
fn it_works() {
assert!(false);
}
这样,只有函数panic!
时才会通过测试,否则就失败了.尝试下:
$ cargo test
Rust还提供了assert_eq!
的宏,来比较2个参数是否相等:
#[test]
#[should_fail]
fn it_works() {
assert_eq!("Hello", "world");
}
这个会通过还是失败呢?因为有should_fail
,所以会通过测试:
$ cargo test
带should_fail
的测试不是很健壮.因为很难保证这个测试不是因为其他预期不到的原因而失败.为了解决这个,should_fail
增加了可选的expected
参数.这就保证了失败的信息必须包含expected
中的字符串.一个更健壮的测试如下:
#[test]
#[should_fail(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模块
我们的例子有一个不太理想的地方:缺少test模块.理想的方式是如下的:
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
属性表示这段代码只有在运行test
时,才会编译运行.这样能节省编译时间,也确保测试代码不会存在在正常的构建之中.
第二个是use
声明.因为我们现在在内部模块中,因此需要把外部的函数引入到当前作用域里.当要测试的外部模块非常大时,这样的use
代码会非常的冗杂,所以可以使用glob
的通配符功能.下面就是src/lib.rs
的代码:
#![feature(globs)]
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));
}
}
注意下feature
属性的使用和use
那行的改变.现在运行测试:
$ cargo test
测试通过!
现在的惯例是使用test
模块来包含单元测试(unit test)代码.任何测试功能的代码都应该放在这个模块里.但集成测试(integration test)怎么办?我们使用test目录来解决它.
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
该测试有3节:之前的测试代码,和新的测试代码.
这就是tests目录的全部.此处test
模块不是必须的,因为此处着眼于集成测试.
(译者: test
模块对模块名称没有要求,但tests
目录的名称必须是tests
)
最后,我们看下上面测试的第3节内容:文档测试
文档测试
没有什么比带使用示例的文档更好的了,也没有什么比错误的使用示例更烂的了,因为文档没有跟着代码一起更新.为此,Rust提供了可以自动运行文档中代码的功能.下面就是带示例的src/lib.rs
:
//! The `adder` crate provides functions that add numbers to other numbers
//!
//! # Examples
//! ```
//! assert_eq(4, adder::add_two(2));
//! ```
#![feature(globs)]
/// This is 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语法,所以此处3个反引号表示代码块.像上面那样带有# Examples
也是很方便的.
运行下测试看看:
$ cargo test
现在我们就有了3种测试了!注意文档测试的名字:_0
是模块级别的测试,而add_two_0
是函数级别的测试.这个名字会根据增加的示例数,自动增长的.
基准测试
Rust还支持基准测试,用来测试代码性能.src/lib.rs
如下:
#![feature(globs)]
extern crate test;
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
use test::Bencher;
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
#[bench]
fn bench_add_two(b: &mut Bencher) {
b.iter(|| add_two(2));
}
}
我们先引入test
包,这里包含了基准测试的工具.我们还有了一个带bench
属性的函数.同其他不带参数的测试不同,基准测试带一个&mut Bencher
的参数.这个Bencher
有一个iter
方法,其参数是一个闭包(closure),闭包的内容就是我们想要基准测试的内容.
通过cargo bench
来运行基准测试:
$ cargo bench
非基准测试就被忽略了.cargo bench
耗时较长.因为Rust需要重复运行多次,最后汇报其平均耗时.此处的测试比较简单,所以显示的是1 ns/iter (+/- 0)
,但如果测试复杂的话,这里会显示出来耗时的.
关于基准测试的建议:
- 将环境搭建(setup)代码放到
iter
外部;iter
里只放你要测试的代码 iter
尽量很次都做一样的事情;不要累加或改变内部状态- 确保外围函数是幂等(idempotent)的;基准测试会运行很多次的
- 尽量让
iter
内部代码又少又快,这样基准测试也会很快的,校准器(calibrator)也能更好的校验误差 - 尽量让
iter
内部代码做一些有助于找出性能瓶颈的简单事情
优化
基准测试有一个难点:带有优化的基准测试会因为优化的不同而有不同的表现,所以可能就不再是当时编写基准测试想要测试的那部分内容了.比如,编译器可能会识别出一些没有副作用的计算,然后直接优化没了.
extern crate test;
use test::Bencher;
#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
b.iter(|| {
range(0, 100).fold(0, |old new| old ^ new);
});
}
结果如下:
running 1 test
test bench_xor_1000_ints ... bench: 0 ns/iter (+/- 0)
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured
(译者: 由于iter
中都是内部计算,无外部变量,可能直接被优化没了,所以上面基准测试都是0ns)
基准测试提供了2种方式来避免这个情况.第一种是让iter
的那个闭包参数返回任意某种值,这样就迫使编译器认为这个返回值被使用了,从而不会把这个代码直接优化没.可以如下来做:
b.iter(|| {
// 去掉了后面的";"
range(0, 100).fold(0, |old new| old ^ new)
});
第二种方法是调用通用的test::black_box
函数,这个相当于引入了一个黑盒(black box),使编译器不能将代码直接优化没.
b.iter(|| {
let mut n = 1000;
test::black_box(&mut n);
range(0, n).fold(0, |a b| a ^ b);
});
这个方法不会读取或修改n
的值,而且对于size较小的类型的值来说,是非常轻量级的.size较大的类型的值,可以间接传递来减少开销(比如black_box(&huge_struct)
).
上面2种方法都能改变基准测试结果:
running 1 test
test bench_xor_1000_ints ... bench: 1 ns/iter (+/- 0)
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured
尽管使用了上面的方法,编译器还是能以一些非预期的方式来优化测试代码的.