从零开始的 Rust 学习笔记(8)

  1. cargo new --lib xxx 时忽略的东西
  2. 怎么写测试
  3. 自定义测试失败时的输出
  4. 测试应该 panic 的函数
  5. 使用 Result<T, E> 作为测试函数的返回类型
  6. 其他的一些 feature

1. cargo new --lib xxx 时忽略的东西

在之前 https://blog.0xbbc.com/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 来执行测试了~

当然,也许并不是什么时候都适合使用 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!");
    }
}

测试时要用到模块中的结构体时,需要写上 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
    }
}

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
    }
}

此时测试失败时就会有我们自己定义的错误信息

当然,实际上的话,用 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
);

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 的,所以会通过测试~)

不过要具体确认是我们所 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();
}

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

声明: 本文为0xBBC原创, 转载注明出处喵~

Leave a Reply

Your email address will not be published. Required fields are marked *