1. cargo new --lib xxx 时忽略的东西
在之前 https://await,moe/2019/09/rust-learning-from-zero-5/ 中,用 cargo new --lib libname
来创建了一个包
在 cargo 默认的 src/lib.rs
模版中,当时其实可以忽略了一段代码
#[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } }
这个其实就是模块测试的代码啦~当然这个用来测试的 mod 的名字自己随便起也是可以的~只需要在测试的 mod 前加上下面这句就可以(。・ω・。)
#[cfg(test)] mod mod_name { // ... }
接下来就是用来测试的函数了,与日常的写法类似,不过要在测试函数前写上 #[test]
即可~
#[cfg(test)] mod mod_name { #[test] fn test_entry1() { // ... } #[test] fn test_entry2() { // ... } }
这样的话,前面有 #[test]
的函数,就会成为测试时的其中一项了~
2. 怎么写测试
那下一个问题就自然而然的变为了——怎么写测试了
在每一个 #[test]
标记的函数里,可以用 assert_eq(a, b)
, assert!(bool)
等判断被测试的函数是否有如期的输出等
#[cfg(test)] mod mod_name { #[test] fn test_entry1() { assert_eq!(2 + 2, 4); } #[test] fn test_entry2() { assert_eq!(2 + 2, 3); } }
在写好测试函数之后,就可以用 cargo test
来执行测试了~
![](/wp-content/uploads/2019/10/rust-8-assert-eq.webp)
当然,也许并不是什么时候都适合使用 assert_eq
来做的,因此还可以用 panic!
~如果在测试中发现没有完成预期的功能时,就可以写 panic!
#[cfg(test)] mod mod_name { #[test] fn test_entry1() { assert_eq!(2 + 2, 4); } #[test] fn test_entry_panic() { // some work... // and check whether if satisfied expectations // if not panic!("test failed!"); } }
![](/wp-content/uploads/2019/10/rust-8-panic.webp)
测试时要用到模块中的结构体时,需要写上 use super::*
~
一个具体的例子如下~
#[cfg(test)] mod mod_name { // let `Triangle` visible to `mod_name` use super::*; #[test] fn can_form_triangle() { let triangle = Triangle {a: 3, b: 4, c: 5}; assert!(triangle.can_be_triangle()); } } #[derive(Debug)] pub struct Triangle { pub a: u32, pub b: u32, pub c: u32 } impl Triangle { pub fn can_be_triangle(&self) -> bool { let ab = self.a + self.b; let bc = self.b + self.c; let ac = self.a + self.c; // sum of any two sides should be strictly greater than the third side ab > self.c && bc > self.a && ac > self.b } }
![](/wp-content/uploads/2019/10/rust-8-test-triangle.webp)
3. 自定义测试失败时的输出
有时候需要自己定义测试失败时的输出,用于debug
因此可以把刚才的代码改为~
#[cfg(test)] mod mod_name { // let `Triangle` visible to `mod_name` use super::*; #[test] fn can_form_triangle() { let triangle = Triangle {a: 3, b: 4, c: 7}; assert!( triangle.can_be_triangle(), "Sum of any two sides should be strictly greater than the third side" ); } } #[derive(Debug)] pub struct Triangle { pub a: u32, pub b: u32, pub c: u32 } impl Triangle { pub fn can_be_triangle(&self) -> bool { let ab = self.a + self.b; let bc = self.b + self.c; let ac = self.a + self.c; // sum of any two sides should be strictly greater than the third side ab > self.c && bc > self.a && ac > self.b } }
此时测试失败时就会有我们自己定义的错误信息
![](/wp-content/uploads/2019/10/rust-8-custom-error-info.webp)
当然,实际上的话,用 assert!
自定义错误信息的完整形式就类似于 print
一样
assert!(bool, fmt, args...);
也就是可以讲上面的 assert!
语句改为如下形式,这样就可以自定义输出更多信息
assert!( triangle.can_be_triangle(), "Sum of any two sides should be strictly greater than the third side: {} {} {}", triangle.a, triangle.b, triangle.c );
![](/wp-content/uploads/2019/10/rust-8-custom-error-fmt.webp)
4. 测试应该 panic 的函数
还有另一种,就是我们要测试的函数应该在某些时候调用 panic!
,测试的时候需要确认这一点
这里的话,Rust 里提供了另一个标记,#[should_panic]
#[cfg(test)] mod mod_name { // let `Triangle` visible to `mod_name` use super::*; #[test] fn can_form_triangle() { let triangle = Triangle {a: 3, b: 4, c: 7}; assert!( triangle.can_be_triangle(), "Sum of any two sides should be strictly greater than the third side: {} {} {}", triangle.a, triangle.b, triangle.c ); } #[test] #[should_panic] fn triangle_first_then_perimeter() { let triangle = Triangle {a: 3, b: 4, c: 7}; let _ = triangle.perimeter(); } } #[derive(Debug)] pub struct Triangle { pub a: u32, pub b: u32, pub c: u32 } impl Triangle { pub fn can_be_triangle(&self) -> bool { let ab = self.a + self.b; let bc = self.b + self.c; let ac = self.a + self.c; // sum of any two sides should be strictly greater than the third side ab > self.c && bc > self.a && ac > self.b } pub fn perimeter(&self) -> u32 { if self.can_be_triangle() { return self.a + self.b + self.c; } else { panic!("This can not be a triangle even!"); } } }
可以看到,此时 triangle_first_then_perimeter()
测试中,边长3、4、7不能组成三角形,调用 perimter()
时会 panic!
,因此我们用 #[should_panic]
标记了 triangle_first_then_perimeter()
测试。在最后测试时的确也有 panic!
,所以测试通过~
(也就是说,测试应该 panic!
的函数时,只要在对应的测试函数中写好了 #[should_panic]
,就没问题,运行测试时内部会有 panic!
,但那是我们 expected 的,所以会通过测试~)
![](/wp-content/uploads/2019/10/rust-8-should-panic.webp)
不过要具体确认是我们所 expected 的 panic!
的话,还可以写上具体期待的 panic!
的内容来让 Rust 测试进行匹配
只需要向如下代码高亮的部分一样,在对应的 #[should_panic]
中增加 expected 的信息即可~
#[test] #[should_panic(expected = "This can not be a triangle even!")] fn triangle_first_then_perimeter() { let triangle = Triangle {a: 3, b: 4, c: 7}; let _ = triangle.perimeter(); }
但是需要注意的是,这里并不是完全相同才算通过,而是类似于正则中的.*expected.*
那么不匹配的时候,报错则大致如下~
#[test] #[should_panic(expected = "Won't match")] fn triangle_first_then_perimeter() { let triangle = Triangle {a: 3, b: 4, c: 7}; let _ = triangle.perimeter(); }
![](/wp-content/uploads/2019/10/rust-8-panic-not-included.webp)
5. 使用 Result<T, E> 作为测试函数的返回类型
还有很多时候的话,比如我们写的 API 返回的是 Result<T, E>
的话,这个时候也许不那么方便去用一个或者几个 assert!
,又或者是 panic!
解决~
因此 Rust 也提供了对 Result<T, E>
的支持
#[cfg(test)] mod mod_name { use super::*; #[test] fn test_perimeter() -> Result<(), String> { let a = 3; let b = 4; let c = 5; let triangle = Triangle {a, b, c}; if let Ok(perimeter) = triangle.perimeter() { if perimeter == a + b + c { Ok(()) } else { Err(String::from("what happened to perimeter()?")) } } else { Err(String::from("This can not be a triangle even!")) } } } #[derive(Debug)] pub struct Triangle { pub a: u32, pub b: u32, pub c: u32 } impl Triangle { pub fn can_be_triangle(&self) -> bool { let ab = self.a + self.b; let bc = self.b + self.c; let ac = self.a + self.c; // sum of any two sides should be strictly greater than the third side ab > self.c && bc > self.a && ac > self.b } pub fn perimeter(&self) -> Result<u32, String> { if self.can_be_triangle() { Ok(self.a + self.b + self.c) } else { Err(String::from("This can not be a triangle even!")) } } }
当我们的测试函数返回的 Result<T, E>
是 Ok(T)
的时候,则认为是没问题的~如果返回了 Err(E)
的话,测试就会失败
6. 其他的一些 feature
最后比较值得提的一些则是,可以并行测试
cargo test -- --test-threads=1
如果想知道函数的结果和我们 expect 的结果分别是多少的话,可以使用如下命令
cargo test -- --nocapture
同时,我们可以只进行一部分的测试,不过只能根据测试函数的名字来筛选,
cargo test name
即 ^name.*
的都进行测试
如果有一些测试有特殊要求,或者就是耗时很长,需要专门处理的话,则可以使用 #[ignore]
标记,然后在合适的时候用如下命令去测试这些~
cargo test -- --ignored