测试

欢马劈雪     最近更新时间:2020-08-04 05:37:59

225

testing.md
commit 6ba952020fbc91bad64be1ea0650bfba52e6aab4

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代码的正确方法。有很多关于写测试好坏方法的流派。所有的这些途径都使用相同的基本工具,所以我们会向你展示他们的语法。

test属性(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的状态码.我们在 OS X和 Linux 中使用$?

$ echo $?
101

在 Windows 中,如果你使用cmd

> echo %ERRORLEVEL%

而如果你使用 PowerShell:

> echo $LASTEXITCODE # the code itself
> echo $? # a boolean, fail or succeed

这在你想把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!是非常常见的;用已知的参数调用一些函数然后与期望的输出进行比较。

ignore属性

有时一些特定的测试可能非常耗时。这时可以通过ignore属性来默认禁用:

#[test]
fn it_works() {
    assert_eq!(4, add_two(2));
}

#[test]
#[ignore]
fn expensive_test() {
    // code that takes an hour to run
}

现在我们运行测试并发现it_works被执行了,而expensive_test没有

$ cargo test
   Compiling adder v0.0.1 (file:///home/you/projects/adder)
     Running target/adder-91b3e234d4ed382a

running 2 tests
test expensive_test ... ignored
test it_works ... ok

test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

耗时的测试可以通过调用cargo test -- --ignored来执行:

$ cargo test -- --ignored
     Running target/adder-91b3e234d4ed382a

running 1 test
test expensive_test ... 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

--ignored参数是 test 程序的参数,而不是 Cargo 的,这也是为什么命令是cargo test -- --ignored

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 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

它能工作了!

目前的习惯是使用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 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目录的全部内容。它不需要test模块因为它整个就是关于测试的。

让我们最后看看第三部分:文档测试。

文档测试

展开阅读全文