测试从来不是一件简单的事情,我本人深有体会!
书本作者引用了很重要的话:软件测试是证明 bug 存在的有效方法,而证明其不存在时则显得令人绝望的不足 (Edsger W. Dijkstra 在其 1972 年的文章【谦卑的程序员】(“The Humble Programmer”))
注:Edsger W. Dijkstra在1972获得图灵奖
本人学过一些语言,看过不少类似书籍,但是在书籍中间部分就让入门者注意测试,的确是挺特别的。
这充分地体现了作者对测试的重视。
既然专家都这么注意测试,我辈自当也需要格外注意。Rust由于其语言特性,应该是需要更加重视测试!
把测试当做无足轻重,或者认为不需要什么技术水平的,都是不专业的行为!it新人或者一些水平有问题的常常会有这个方面的看法!
本章节的内容比较多,但只要按照作者的例子作一遍,那么基本能够掌握!
rust测试需要用到cargo test命令,这个命令有许多选项,部分会在本章节中提到,还有许多是没有提到的。
把test命令的各个选项都熟悉下,也需要耗费不少时间!
本文所涉及到内容都是入门级别的,属于基本的粗浅的。但是掌握这些基本上已经可以做需要的测试了。
至于测试技巧,是另外一门学问了,和rust没有关系。
一、编写单元测试代码
本例模仿书上的,在库中编写单元测试代码。
按照惯例:创建一个tests模块,并在tests下编写测试代码
关键词:
#[cfg(test)] -- 告诉编译器运行的时候不要编辑
#[test] -- 告诉编译器这是一个测试单元
函数: assert_eq!,assert_ne!,assert!
二、控制测试
关键问题:
- 如何控制并发
- 如何控制需要测试的
- 如何忽略,或者如何只测试被标记为忽略的
控制并发
默认是多线程,但也可以通过一下方式指定需要的线程
控制测试目标
忽略
cargo test -- --ignored 只测试被忽略的
三、测试的组织结构
如何做单元测试
如何做集成测试
创建和src同i的目录tests,在tests下放任意个rs,每个rs都是一个单元包
每个单元包都可以应用tests的公共模块。
集成测试中的公共模块
使用老的模块定义规范:目录+mod.rs
如上图,haha模块在tests/haha下,其下有mod.rs
四、完整测试代码和示例
这个例子是基于一个库项目,有三个文件:
src/lib.rs 库 ,单元测试文件
tests/itest1.rs 集成测试文件之一
tests/haha/mod.rs 集成测试的模块haha
lib.rs
pub fn add(left: u64, right: u64) -> u64 {left + right }#[cfg(test)] mod tests {use super::add;//测试宏 assert_eq! #[test]fn it_works1() {let result = add(2, 2);assert_eq!(result, 4);}//测试宏 assert!.并测试自定义的错误信息 #[test]fn it_works2() {let result = add(1, 3);assert!(result==5,"1+3={},而不是5",result);}//测试宏 assert_ne! #[test]fn it_works3() {let result = add(1, 4);println!("这一句将在--show-output 的时候打印:1+4={}",result);assert_ne!(result,6);}//测试fail! #[test]#[should_panic]fn it_fail() {//fail!("this test will fail");let result = add(1, 4);assert_eq!(result,6);}#[test]#[should_panic]fn access_outof_range() {let mut v = vec![1,2]; v[2] = 3;} //如果返回Err,则测试失败 #[test]fn test_result()->Result<(),String> {if 1==2 {Ok(())} else {Err("error".to_string())}}#[test]#[ignore]fn test_ignore() {assert_eq!(1,2, "1!={}",2);} }
itest1.rs
use lzfmath::*; mod haha;#[test] fn itest_add() {haha::haha();assert_eq!(add(1, 2), 3); }
mod.rs
pub fn haha(){println!("哈哈!"); }
4.1标准单元测试结果
4.2设置并发和查看标准输出
4.3只测试it开头的单元测试
4.4只测试被忽略的
4.5集成测试+集成测试模块
五、小结
代码测试主要有三个部分内容:
1.如何编写测试
主要涉及到几个编译标记/指令和几个函数/宏,Result
编译指令
a.#[cfg(test)] 一个条件编译指令,用于指定紧随其后的代码块或项(如函数、模块等)仅在测试环境下编译
b.#[test] 函数测试标记,cargo的测试的时候,会找出有这些标记的函数,并执行测试
c.#[should_panic] 函数测试标记,标记的函数会期待panic,如果函数没有panic就会测试失败。
函数
a.assert! 断定表达式为真。如果为真就是成功
b.assert_eq! 断定两个值是否相等。如果相等就是成功
c.assert_ne! 断定两个值是否不相等。如果不相等就是成功
d.assert_matches! 断定表达式匹配模式。如果匹配就是成功.特别适用于枚举和结构等类型
f.fail! 断定测试失败,并打印出字符串
assert类函数,可以在标准参数之外再传递其它参数,比如失败时打印的字符串。rust会调用格式化字符串,并打印出参数。
Result类型
a.Result<T,E> 枚举类型,表示成功或失败。T为成功时的类型,E为错误类型
2.如何控制测试
默认的测试行为
a.cargo test 在测试模式下编译代码并运行生成的测试二进制文件
b.cargo test 产生的二进制文件的默认行为是并发运行所有的测试,并截获测试运行过程中产生的输出,阻止它们被显示出来,使得阅读测试结果相关的内容变得更容易
如何控制(控制cargo test的行为)
a.控制并发 -- --test-threads=1 只允许一个线程运行测试
b.控制输出 -- --show-output 显示测试运行过程中产生的输出。这样可以输出通过部分的打印输出
c.控制测试 <name> 只运行指定名称的测试 。注意这是模糊匹配
add 测试包含add的测试。即cargo test add
--test xxx 指定具体文件名 cargo test --test itest1 测试文件名带itest1的测试
忽略项目
a.#[ignore] 在代码中标记需要忽略的内容
b.测试被忽略项目 -- --ignored ,这样只会测试被标记为#[ignore]的内容
示例
cargo test result -- --test-threads=2 --show-output 在开启两个线程的情况下,测试包含result的测试,同时那些产生的输出也会显示出来
cargo test -- --ignored 测试被标记为#[ignore]的内容
3.测试的组织结构
如何作单元测试
a.固定的规范-- 使用#[cfg(test)] 标记测试代码,并使用#[test]标记测试函数。其次模块通常也命名位tests
如何作集成测试
a.在项目下创建tests目录,然后在该目录下创建测试文件 。注意tests目录和src是同级的. 这个目录应该是可以是别的,也许可以通过test的选项来指定
b.在测试目录tests可以创建任意多个文件,cargo会把每一个文件当做一个单元包进行测试
c.运行 cargo test即可,但是这样会把单元测试一起执行。
d.通过指定集成测试文件的方式,可以只运行指定的测试文件 cargo test --test itest1.也可以通过指定测试名称的方式
carggo test xxx
综合起来,cargo test --test xxx 也许是更好的选择。
集成测试的公共模块
a.必须先创建一个目录,假设是haha
b.在haha下创建mod.rs文件
c.tests下的其它rs可以调用haha,并且haha不会被测试。haha的内容只会被集成测试代码调用。
也就是说集成测试目录下的模块组织方式,反而使用的是旧的模块定义规范:基于目录+mod.rs
为什么很多项目既有src/main.rs,src/lib.rs
这都是为了方便测试。
引入在集成测试中导入src/main.rs的模块并不容易。 main.rs是一个二进制文件,而lib.rs是库。