Rust从入门到实战系列一百零八:编写自动化测试

03-10 1619阅读

commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f

Edsger W. Dijkstra 在其 1972 年的文章【谦卑的程序员】(”The Humble Programmer”)中说到 ” 软件测试是证明 bug 存在的有效方法,而证明其不存在时则显得令人绝望的不足。”(”Program testing can

Rust从入门到实战系列一百零八:编写自动化测试
(图片来源网络,侵删)

be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showingtheir absence.”)这并不意味着我们不该尽可能地测试软件!

程序的正确性意味着代码如我们期望的那样运行。Rust 是一个相当注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此,Rust 也在语言本身包含了编写软件测试的支持。

例如,我们可以编写一个叫做 add_two 的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的类型检查和借用检查,例如,这些检查会确保我们不会传递 String 或无效的引用给这个函数。Rust 所 不能检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。

我们可以编写测试断言,比如说,当传递 3 给 add_two 函数时,返回值是 5。无论何时对代码进行修改,都可以运行测试来确保任何现存的正确行为没有被改变。

测试是一项复杂的技能:虽然不能在一个章节的篇幅中介绍如何编写好的测试的每个细节,但我们还是会讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。

如何编写测试

commit b9a473ff80e72ed9a77f97a80799b5aff25b594a

Rust 中的测试函数是用来验证非测试代码是否按照期望的方式运行的。测试函数体通常执行如下三种操作:

  1. 设置任何所需的数据或状态
  2. 运行需要测试的代码
  3. 断言其结果是我们所期望的

    让我们看看 Rust 提供的专门用来编写测试的功能:test 属性、一些宏和 should_panic 属性。

    测试函数剖析

    作为最简单例子,Rust 中的测试就是一个带有 test 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据;第五章中结构体中用到的 derive 属性就是一个例子。为了将一个函数变成测试函数,需要在 fn 行之前加上 #[test]。当使用 cargo test 命令运行测试时,Rust 会构建一个测试执行程序用来调用标记了 test 属性的函数,并报告每一个测试是通过还是失败。

    第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。当然你也可以额外增加任意多的测试函数以及测试模块!

    我们会通过实验那些自动生成的测试模版而不是实际编写测试代码来探索测试如何工作的一些方面。接着,我们会写一些真正的测试,调用我们编写的代码并断言他们的行为的正确性。

    让我们创建一个新的库项目 adder:

Created library `adder` project
$ cd adder
adder 库中 src∕ lib .rs 的内容应该看起来如示例 11-1 所示:
文件名: src∕lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

现在让我们暂时忽略 tests 模块和 #[cfg(test )] 注解,并只关注函数来了解其如何工作。注意 fn 行之前的 #[test]:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。因为也可以在tests 模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用 #[test] 属性标明哪些函数是测试。

函数体通过使用 assert_eq! 宏来断言 2 加 2 等于 4。一个典型的测试的格式,就是像这个例子中的断言一样。接下来运行就可以看到测试通过。

cargo test 命令会运行项目中所有的测试.

Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.57s
Running unittests (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in

Cargo 编译并运行了测试。在 Compiling、Finished 和 Running 这几行之后,可以看到 running 1 test这一行。下一行显示了生成的测试函数的名称,它是 it_works,以及测试的运行结果,ok。接着可以看到全体测试运行结果的摘要:test result: ok. 意味着所有测试都通过了。1 passed; 0 failed 表示通过或失败的测试数量。

因为之前我们并没有将任何测试标记为忽略,所以摘要中会显示 0 ignored。我们也没有过滤需要运行的测试,所以摘要中会显示0 filtered out。在下一部分 ” 控制测试如何运行” 会讨论忽略和过滤测试。

0 measured 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust开发版(nightly Rust)。请查看 性能测试的文档 了解更多。

测试输出中的以 Doc−tests adder 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 ” 文档注释作为测试” 部分会讲到如何编写文档测试。现在我们将忽略 Doc−tests 部分的输出。

让我们改变测试的名称并看看这如何改变测试的输出。给 it_works 函数起个不同的名字,比如exploration,像这样:

mod tests {
#[test]
fn exploration() {
assert_eq!(2 + 2, 4);
}
}
并再次运行 cargo test。现在输出中将出现 exploration 而不是 it_works:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.59s
Running unittests (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in

让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。

每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。第九章讲到了最简单的造成 panic 的方法:调用 panic! 宏。

#[cfg(test)]

mod tests {

#[test]

fn exploration() {

assert_eq!(2 + 2, 4);

}

#[test]

fn another() {

panic!(“Make this test fail”);

}

}

再次 cargo test 运行测试。它表明 exploration 测试通过了而 another 失败了:
```$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.72s
Running unittests (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'

test tests :: another 这一行是 FAILED 而不是 ok 了。在单独测试结果和摘要之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,another 因为在 src∕lib.rs 的第 10 行

panicked at ’Make this test fail ’ 而失败。下一部分列出了所有失败的测试,这在有很多测试和很多失败测试的详细输出时很有帮助。我们可以通过使用失败测试的名称来只运行这个测试,以便调试;下一部分 ” 控制测试如何运行” 会讲到更多运行测试的方法。

最后是摘要行:总体上讲,测试结果是 FAILED。有一个测试通过和一个测试失败。

现在我们见过不同场景中测试结果是什么样子的了,再来看看除 panic! 之外的一些在测试中有帮助的宏吧。

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]