Home

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),但如果测试复杂的话,这里会显示出来耗时的.

关于基准测试的建议:

优化

基准测试有一个难点:带有优化的基准测试会因为优化的不同而有不同的表现,所以可能就不再是当时编写基准测试想要测试的那部分内容了.比如,编译器可能会识别出一些没有副作用的计算,然后直接优化没了.

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

尽管使用了上面的方法,编译器还是能以一些非预期的方式来优化测试代码的.