Rust能力养成之(14)单元测试

图片

 

前言

 

上一篇,我们讲了 构成测试的一些组件,本篇开始来讲一下单元测试。

一般来说,单元测试是一个函数,用来实例化应用程序的一小部分,并独立于代码基的其他部分验证待测行为。在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<((i8, i8), i8)> {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)

 

 

主要参考和建议读者进一步阅读的文献

https://doc.rust-lang.org/book

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值