Rust 基础知识18 - 编写自动化测试

简介

  • 接上回书
  • 测试的重要性不言而喻,可以看一些TDD的相关知识,不赘述。
  • cargo + Rust 天生支持测试,这个特性还是非常好的,省得安装其他的测试组件了。

如何把函数变成测试函数

  • 测试函数需要使用test属性(attribute)进行标注。
  • 就是在函数上面加上 #[test],Rust就会把这个函数当成测试函数

运行测试

  • 使用命令cargo test命令运行所有测试函数,Rust会构建一个Test Runner 可执行文件,它会运行标注了 #[test] 的函数,并报告其是否通过测试。
  • 写一个独立的测试函数,cargo test 没问题:
#[test]
fn test_eq() {
    assert_eq!(2+ 2 , 4)
}
  • 标准一点可以写一个 #[cfg(test)] 模块
#[cfg(test)]
mod tests {
    #[test]
    fn test_eq() {
        assert_eq!(2+ 2 , 4)
    }

    #[test]
    fn test_eq2() {
        assert_eq!(2+ 2 , 5)
    }
}

image.png
  • 测试失败可以使用 panic 函数,就表示失败。
  • 每个测试运行在一个新线程里。

断言 assert!() 宏

  • assert! 宏来自标准库,用来确定某个状态是否为true,true表示通过,false就会调用 panic! 测试失败。
  • 用之前的代码举例,测试一下 Rectangle can_hold
  • 新建文件 rectangle.rs 直接可以把相关的测试写到这个文件里,这是没有问题的,写文件里就不要加 mod test了没必要,举例如下:
// 定义一个矩形
pub struct Rectangle {
    width: u32,
    height: u32,
}
// 实现相关方法
impl Rectangle {
    // 判断某个矩形是否可以放到当前矩形中
    pub fn can_hold(&self, o: &Rectangle) -> bool {
        if self.width >= o.width && self.height >= o.height {
            true
        } else {
            false
        }
    }
}

// #[cfg(test)] // 如果放到模块文件中,那么就不推荐使用 mod test 了
// mod test {
// }
#[test]
fn can_hold() {
    let a = Rectangle {
        width: 10,
        height: 20,
    };
    let b = Rectangle {
        width: 5,
        height: 20,
    };
    // 断言
    assert!(a.can_hold(&b));
    assert!(!b.can_hold(&a));
}

  • 执行截图:


    image.png

使用 assert_eq! 和 assert_ne! 测试

  • 都来自标准库。
  • 判断两个参数是否相等或者不等。
  • 要求参数实现了PartialEq和Debug Traits
  • 举例:
assert_eq!(4, 3);
assert_ne!(4, 3);

添加自定义的错误信息

  • 可以向 assert!、assert_eq!、assert_ne! 添加自定义参数。
  • 其实就是在后面加上字符串字面值。

用should_panic 检查恐慌

  • 测试除了验证代码的返回值是否正确,还需验证代码是否如期处理发生错误的情况。
  • 可验证代码在特定情况下是否发生panic
  • 使用方式:#[test] + #[should_panic]
#[test]
#[should_panic]
fn greater_than_100 () {
    ...
}

让 should_panic 更精准

  • 很多种情况都可能包含 panic,这是可以添加一个可选的 expected 参数:
#[test]
#[should_panic(expected = "Some word.")]
fn test_method() {
  ...
}

在测试中使用 Result

  • 无需 panic,可以使用 Result 作为返回类型编写测试,如果返回 Ok 测试通过,返回 Err 测试失败。
  • 不要在使用 Result 编写的测试上标注 #[should_panic] ,举例

#[test]
fn test_result ()->Result<(), String> {
    if 2+1 == 4 {
        Ok(())
    } else {
        Err(String::from("hello"))
    }
}`

如何控制测试的运行方式

  • cargo test 默认上并行运行测试,不显示所有输出,除非出现错误断言的时候,主要是为了方便读取。
  • 如果不想并行运行测试,需要添加 --test-threads
  • 例如:cargo test -- --test-threads=1
  • 如果想输出所有打印信息可以使用 --show-output
  • 例如: cargo test -- --show-output

按名称运行测试

  • 选择运行的测试,将测试的名称(一个或多个),作为cargo test 的参数
  • 例如:
#[test]
fn test_add_1 () {
    assert!(2+2 == 4)
}

#[test]
fn test_add_2 () {
    assert!(2+2 == 4)
}
#[test]
fn test_add_3 () {
    assert!(2+2 == 4)
}

  • 如果输入 cargo test


    image.png
  • 如果输入 cargo test add


    image.png
  • 如果输入 cargo test 1


    image.png
  • 所以就是 cargo test match_str 会进行模糊匹配

忽略某些测试

  • 某些情况下,某些测试我们需要进行忽略,比如那些执行的非常慢的,我们不希望他们每次都运行。
#[test]
fn test_add_1 () {
    assert!(2+2 == 4)
}

#[test]
fn test_add_2 () {
    assert!(2+2 == 4)
}
#[test]
#[ignore]
fn test_add_3 () {
    assert!(2+2 == 4)
}
  • 执行 cargo test add 返回结果如下(注意黄线部分):
image.png
  • 可以单独添加 --ignore 参数运行被忽略的测试。
  • 命令行参数是:cargo test -- --ignored 注意不要忘了中间的两个 -- 切记:
  • 返回结果如下:


    image.png

测试组织形式

  • Rust 对测试的分类
1、单元测试。
2、集成测试。
  • 单元测试关注的小,且专注,一次对一个模块进行隔离测试,可以测试 private 的接口。
  • 继承测试,在库的外部,和其他外部代码一样,但是只能测试public 的接口,可能在每个测试中使用多个模块。

单元测试,每个单元文件建立一个test 模块

  • 单元测试的标准写法需要建立一个 tests 模块,并进行 #[cfg(test)]标注:
  • 上面的例子修改一下大致就是这个样子:

#[cfg(test)]
mod test {
    #[test]
    fn test_add_1() {
        assert!(2 + 2 == 4)
    }

    #[test]
    fn test_add_2() {
        assert!(2 + 2 == 4)
    }
    #[test]
    #[ignore]
    fn test_add_3() {
        assert!(2 + 2 == 4)
    }
}

  • 只有运行cargo test 才会编译和运行 #[cfg(test)] 。

集成测试

  • 在Rust 里,集成测试完全位于被测试库的外部,目的是测试被测试库的多个部分是否能正确的一起工作。

  • 集成测试的覆盖率很重要。

  • 集成的是需要创建一个 tests 的目录。

  • tests 目录下的每个测试文件都是单独的一个 carte ,需要将被测试库导入。

  • 无需标注 #[cfg(test]) ,tests 目录被特殊对待,只要 cargo test 才会编译 tests 目录下的文件。

  • 需要创建test目录,该目录和 src 目录平级即可。


    image.png
  • 为了让程序可以测试,首先,修改一下 rectangle.rs,主要是新增 new 方法,让外部可以访问当(让 test 继承测试访问到),修改后的代码如下:

use std::sync::mpsc::Receiver;

// 定义一个矩形
pub struct Rectangle {
    width: u32,
    height: u32,
}


impl Rectangle {
    pub fn new(width:u32, height:u32) -> Rectangle {
        Rectangle {
            width,
            height,
        }
    }
    // 判断某个矩形是否可以放到当前矩形中
    pub fn can_hold(&self, o: &Rectangle) -> bool {
        if self.width >= o.width && self.height >= o.height {
            true
        } else {
            false
        }
    }
}

  • 然后修改 lib.rs 开放这个mod ,否则 tests 目录的测试代码可能达不到这个模块。
// 新增 rectangle 然后加上 pub 给他公开出去。
pub mod rectangle;
  • 然后在 tests 目录下新增 demo_test.rs 随便写一写测试代码:
use hello::rectangle::Rectangle;

// }
#[test]
fn can_hold_integration_test() {
    let a = Rectangle::new(10, 29);
    let b = Rectangle::new(20, 30);
    assert!(false == a.can_hold(&b));
    assert!(true == b.can_hold(&a));
}
image.png
  • 然后cargo test 试一下:
  • 可以看到集成测试目录 test 被成功执行:


    image.png

集成测试中的子模块

  • tests 目录下的每个文件被编译成单独的cate,这些文件不共享行为,这与src 下的文件规则不同。
  • 举例我们可以写一个 setup() 函数,并将其放到测试中的某个公用位置,比如 common/mod.rs (注意 common 是一个目录的话里面必须要包含入口文件 mod.rs):
  • mod.rs :
pub fn setup() {}
  • 之后修改一下大致如下图:


    image.png

真对binary crate的继承测试

  • 如果项目是 binary crate 只包含 src/main.rs 没有 src/lib.rs 是不能在tests目录下创建继承测试,因为没必要,如果项目比较大肯定是要拆成library crate 然后暴露 crate 的。
  • binary crate 意味着独立运行。

结束

  • 感谢阅读,下篇见。

你可能感兴趣的:(Rust 基础知识18 - 编写自动化测试)