前言
上一篇,我们讲了 构成测试的一些组件,本篇开始来讲一下单元测试。
一般来说,单元测试是一个函数,用来实例化应用程序的一小部分,并独立于代码基的其他部分验证待测行为。在Rust中,单元测试通常是在模块中编写的。理想情况下,应该只针对模块的功能及其接口。
单元测试
第一个单元测试
先来一个最简单的单元测试代码:
// first_unit_test.rs
#[test] fn basic_test() { assert!(true); }
可见,单元测试被编写为一个函数,并用#[test]属性标记,而basic_test函数没有什么复杂的地方。我们有一个基本assert!宏调用传递参数为true。为了更好的组织测试过程,还可以创建一个名为tests的子模块(按照基本约定),并将所有相关的测试代码放在其中。
运行测试
运行测试要在测试模式下。除非被告知以测试模式构建编译,否则编译器不会主动编译测试代码部分,这可以通过在编译测试代码时将--test标志(flag)传递给rustc来实现。接下来,只需执行编译后的二进制文件就可以运行测试。对于前面的测试,我们将通过运行以下命令在测试模式下进行编译
rustc --test first_unit_test.rs
使用--test标志,rustc将一个带有一些测试工具代码与主函数放在一起,并以线程的形式并行调用所有已定义的测试函数。默认情况下,所有测试都并行运行,除非用环境变RUST_TEST_THREADS=1来告知要在单线程模式下运行前面的测试。这个时候我们键入:
first_unit_test
结果如下
至此,我们用最原始的命令,完成一段最原始的单元测试。现在,Cargo已经支持运行测试,所有这些通常都是通过调用Cargo test在内部完成的:这个命令为我们编译并运行带有测试注释的函数。在接下来的示例中,我们将主要使用Cargo来运行测试。
分离测试代码(Isolating test code)
当我们的测试变得越来越复杂时,可能需要创建一些只在测试代码上下文(context)中使用的辅助方法。在这种情况下,将测试相关的代码与实际的代码隔离开来是有意义的。那么,我们可以将所有与测试相关的代码封装在一个模块中,并在上面添加#[cfg(test)]注释。
属性#[cfg(…)]中的cfg通常用于条件编译,而不仅仅局限于测试代码,可以用来包含或排除不同架构或配置标志的代码。这里,配置标志是test。我们在上一节的原始测试中已经在使用这种形式了。这样做的好处是,测试代码只在运行cargo test时,才会被编译并包含在编译后的二进制文件中,否则会被忽略。
在实际场景中,开发者会以编程方式为测试生成测试数据,但是没有理由或必要在发布版本构建中也包含这些代码。让我们通过运行cargo new unit_test--lib来创建一个项目来演示一下。在lib.rs中,我们定义了一些测试和函数:
// unit_test/src/lib.rs
// function we want to testfn sum(a: i8, b: i8) -> i8 {a + b}
#[cfg(test)]mod tests {fn sum_inputs_outputs() -> Vec {vec![((1, 1), 2), ((0, 0), 0), ((2, -2), 0)]}
#[test]fn test_sums() {for (input, output) in sum_inputs_outputs() {assert_eq!(crate::sum(input.0, input.1), output);}}}
我们看下上述代码:我们在sum_inputs_outputs函数中生成已知的输入和输出对,该函数由test_sum函数使用。#[test]属性将test_sum函数排除在发布编译之外。但是,sum_inputs_outputs部分没有标记为#[test],这说明,如果在测试模块test之外声明,将被包含在编译中。通过使用#[cfg(test)]和mod tests{}子模块,并将所有的测试代码及其相关函数封装在这个模块中,可以保持代码和生成的二进制文件中测试代码的泾渭分明。而且还把sum函数定义为private,没有pub可见性修饰符,这意味着模块内的单元测试也允许测试私有函数和方法。
键入命令cargo test,运行结果如下。
失败测试(Failing tests)
还有一些测试用例,用于当API方法出现一些输入失败时,由测试框架来断言(assert)这个失败的情况。Rust为此提供了一个名为#[should_panic]的属性。下面是一个使用这个属性的测试:
// panic_test.rs
// compile in test mode: `rustc --test panic_test.rs`// run tests using: `./panic_test`
#[test]#[should_panic]fn this_panics() { panic!("Succeeded in failing!");}
我们键入 rustc --test panic_test.rs,运行测试,然后执行生成文件panic_test.exe,结果如下
#[should_panic]属性可以与#[test]属性成对出现,表示运行this_panics函数可能导致不可恢复的失败,这被称为在Rust称为panic。
忽略测试(Ignoring tests)
编写Rust测试中,另一个有用属性是#[ignore]。如果测试代码体量非常繁重,#[ignore]注释将使测试工具在运行cargo测试时忽略这些测试函数。然后,可以通过向测试运行程序或cargo test命令提供一个--ignored的参数来选择单独运行这些测试。下面的代码包含一个silly loop,当使用cargo test运行时,默认情况下,会忽略之
// silly_loop.rs
// compile in test mode: `rustc --test ignored_test.rs`// run tests using: `./ignored_test`
pub fn silly_loop() {for _ in 1..1_000_000_000 {};}
#[cfg(test)]mod tests {#[test]#[ignore] pub fn test_silly_loop() { ::silly_loop(); }}
请读者注意一下,在test_sily_loop测试函数上的#[ignore]属性。下面是对应的测试输出:
结语
下一篇,我们讲集成测试(Integration tests)。
主要参考和建议读者进一步阅读的文献
1.Rust编程之道,2019, 张汉东
2.The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger
3.Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger
4.Beginning Rust ,2018,Carlo Milanesi
5.Rust Cookbook,2017,Vigneshwer Dhinakaran