(十)Rust_编写与运行测试


编写与运行测试

测试(函数)

  1. 测试的目的是为了验证非测试代码的功能是否与预期一致。
  2. 测试函数体通常执行 3 个操作:
    • 测试数据/状态。
    • 运行被测试的代码。
    • 断言结果。

测试函数结构

  1. 测试函数需要使用 test 属性(attribute)进行标注:
    • Attribute 就是一段 Rust 代码的元数据,不会改变已有代码的逻辑。
    • 在函数上加 #[test],可把函数变成测试函数。

运行测试

  1. 使用 cargo test 命令运行所有测试函数。

    • Rust 会构建一个 Test Runner 可执行文件。
      • 它会运行标注了 test 的函数,并报告其是否运行成功。
  2. 当使用 cargo 创建 library 项目时,会生成一个 test module,包含来一个 test 函数。

    • 可以添加任意数量的 test module 或函数。
    #[cfg(test)]
    mod tests {
        #[test]
        fn it_works() {
            let result = 2 + 2;
            assert_eq!(result, 4);
        }
    }
    

测试失败

  1. 测试函数 panic 就表示失败。

  2. 每个测试运行在一个新线程。

  3. 当主线程看见某个测试线程挂掉来,那个测试就标记为失败。

    #[test]
    fn another() {
        panic!("Make this test fail!!!")
    }
    
  4. 还可以使用 assert! 宏来检查测试结果,其来自标准库,用来确定某个状态是否为 true。

    • true 表示通过。
    • false 表示失败。
    #[derive(Debug)]
    pub struct Rectangle {
        length: u32,
        width: u32,
    }
    
    impl Rectangle {
        pub fn can_hold(&self, other: &Rectangle) -> bool {
            self.length > other.length && self.width > other.width
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn larger_can_hold_smaller() {
            let larger = Rectangle {
                length: 8,
                width: 7,
            };
            let smaller = Rectangle {
                length: 5,
                width: 1,
            };
            assert!(larger.can_hold(&smaller));
        }
    }
    
  5. 使用 assert_eq!assert_ne! 测试相等性:

    • 都来自标准库。

    • 判断两个参数是否相等不等

    • 实际上,它们使用的就是 ==!= 运算符。

    • 特点是当断言失败时,这两个宏可以打印参数当值。

      • 使用 debug 格式打印参数:要求参数实现了 PartialEqDebug Traits
        (所有当基本类型和标准库里大部分类型都实现了)。

        pub fn add_two(a: i32) -> i32 {
            a + 2
        }
        
        #[cfg(test)]
        mod tests {
            use super::*;
            #[test]
            fn it_adds_two() {
                assert_eq!(4, add_two(2))
            }
        }
        

添加自定义的错误信息

  1. 可以向 assert!assert_eq!assert_ne! 添加可选的自定义消息:

    • 这些自定义消息和失败消息都会打印出来。
    • assert!:第一个参数必填,自定义消息作为第二个参数。
    • assert_eq!assert_ne!:前两个参数必填,自定义消息作为第三个参数。
    • 自定义消息参数会被传递给 format! 宏,可以使用 {} 占位符。
     /*
     running 1 test
     thread 'tests::greetings_contain_name' panicked at 'Greeting didn't contain name, value was 'Hello Carl!'', src/
     lib.rs:12:9
     note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
     test tests::greetings_contain_name ... FAILED
     */
    pub fn greeting(name: &str) -> String {
        format!("Hello {}!", name)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn greetings_contain_name() {
            //let result = greeting("Carol");
            let result = greeting("Carl");
            assert!(
                result.contains("Carol"),
                "Greeting didn't contain name, value was '{}'",
                result
            );
        }
    }
    

检查 panic

验证错误处理的情况

  1. 测试除了验证代码的返回值是否正确,还需要验证代码是否如预期的处理了发生错误的情况。

  2. 可验证代码在特定情况下是否发生了 panic。

  3. should_panic 属性(attribute):

    • 函数 panic:测试通过。
    • 函数没有发生 panic:测试失败。
    /*
    running 1 test
    test tests::greater_than_100 - should panic ... FAILED
    
    failures:
    
    ---- tests::greater_than_100 stdout ----
    note: test did not panic as expected
    
    failures:
        tests::greater_than_100
    */
    pub struct Guess {
        value: u32,
    }
    
    impl Guess {
        pub fn new(value: u32) -> Guess {
            if !(1..=100).contains(&value) {
                panic!("Guess value must be between 1 and 100, got {}", value)
            }
            Guess { value }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        #[should_panic]
        fn greater_than_100() {
            Guess::new(10);
        }
    }
    
  4. should_panic 属性添加一个可选的 expected 参数:

    • 将检查失败消息中是否包含指定的文字,使得测试目标更加精确。
    /*
    尽管都发生了 panic 但是由于检查的匹配字符串不同依旧会测试失败
    ---- tests::greater_than_100 stdout ----
    note: panic did not contain expected string
        panic message: `"Guess value must be greater than or equal 1, got 0"`,
    expected substring: `"Guess value must be less than or equal 100"`
    
    failures:
        tests::greater_than_100
    */
    pub struct Guess {
        value: u32,
    }
    
    impl Guess {
        pub fn new(value: u32) -> Guess {
            if value < 1 {
                panic!("Guess value must be greater than or equal 1, got {}", value)
            } else if value > 100 {
                panic!("Guess value must be less than or equal 100, got {}", value)
            }
            Guess { value }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        #[should_panic(expected = "Guess value must be less than or equal 100")]
        fn greater_than_100() {
            Guess::new(0);
        }
    }
    

在测试中使用 Result<T, E>

  1. 无需 panic,可使用 Result<T, E> 作为返回类型编写测试:

    • 返回 OK:测试通过。
    • 返回 Err:测试失败。
    • 注意:不要在使用 Result 编写的测试用例上标注 #[should_panic]
    /*
    Error: "two plus two does not euqal four"
    thread 'tests::it_works' panicked at 'assertion failed: `(left == right)`
    left: `1`,
    right: `0`: the test returned a termination value with a non-zero status code (1) which indicates a failure', /
    rustc/7737e0b5c4103216d6fd8cf941b7ab9bdbaace7c/library/test/src/lib.rs:187:5
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    test tests::it_works ... FAILED
    */
    #[cfg(test)]
    mod tests {
        #[test]
        fn it_works() -> Result<(), String> {
            if 2 + 3 == 4 {
                Ok(())
            } else {
                Err(String::from("two plus two does not euqal four"))
            }
        }
    }
    

控制测试如何运行

  1. 通过添加命令行参数可以改变 cargo test 命令的行为。
    • 默认行为:
      • 并行运行。
      • 执行所有测试。
      • 捕获(不显示)所有输出(在测试成功的情况下),使读取与测试结果相关的输出更容易。
  2. 命令行参数:
    • 针对 cargo test 的参数:紧跟 cargo test 后。
    • 针对测试可执行程序:放在 -- 之后,例如 cargo test --help
    • cargo test -- --help 可以显示出所有可以应用于 -- 之后的参数。

并行/串行(连续)运行测试

  1. 并行运行:默认使用多个线程并行运行:

    • 优势:运行速度块。
    • 但是需要确保测试之间:
      • 不会相互依赖。
      • 不依赖于某个共享状态(环境、工作目录、环境变量等等)。
  2. 如果不想并行运行,或控制并行的线程数量使用: --test-threads 参数。

    • 传递给二进制的参数。
    • 可以控制测试程序的线程数量。
    • 例如:cargo test -- --test-threads=1

显式函数输出

  1. 默认测试程序对于测试成功的函数会捕获即不显示输出,只有当测试失败时才会打印出来。
    • 使用 --show_output 可以控制测试程序显示成功的输出。

通过测试名称运行测试

  1. 选择运行的测试:将测试的名称(一个或多个)作为 cargo test 的参数。

    • 运行单个测试:指定测试名称,例如 cargo test test_func_name
    • 运行多个测试:指定测试名称的一部分(模块名也可以),例如 cargo test it,其中 it 是两个测试名称都包含的部分。
    /*
    running 2 tests
    test tests::it_works ... ok
    test tests::it_not_work ... ok
    
    test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    */
    #[cfg(test)]
    mod tests {
        #[test]
        fn it_works() -> Result<(), String> {
            if 2 + 2 == 4 {
                Ok(())
            } else {
                Err(String::from("two plus two does not euqal four"))
            }
        }
        #[test]
        fn it_not_work() -> Result<(), String> {
            if 3 + 3 == 6 {
                Ok(())
            } else {
                Err(String::from("two plus two does not euqal four"))
            }
        }
    }
    

忽略某些测试

  1. ignore 属性(Attribute)。

  2. 想要运行被忽略的程序可以使用 --ignored 参数,例如 cargo test -- --ignored

    #[cfg(test)]
    mod tests {
        #[test]
        #[ignore = "long time"]
        fn it_works() -> Result<(), String> {
            if 2 + 2 == 4 {
                Ok(())
            } else {
                Err(String::from("two plus two does not euqal four"))
            }
        }
        #[test]
        fn it_not_work() -> Result<(), String> {
            if 3 + 3 == 6 {
                Ok(())
            } else {
                Err(String::from("two plus two does not euqal four"))
            }
        }
    }
    

测试的组织

测试的分类

  1. Rust 对测试的分类:
    • 单元测试。
    • 集成测试。
  2. 单元测试:
    • 小、专注。
    • 一次对一个模块进行隔离测试。
    • 可测试 private 接口。
  3. 集成测试:
    • 在库外部。和其他外部代码一样使用你的代码。
    • 只能使用 public 接口。
    • 可能在每个测试中使用到多个模块。

单元测试

  1. 单元测试需要使用 #[cfg(test)] 标注。
    • 只用运行 cargo test 才会编译和运行代码。
    • 运行 cargo build 则不会。
  2. 集成测试在不同的目录,它不需要 #[cfg(test)] 标注。
  3. cfg:configuration(配置):
    • 告诉 Rust 下面的条目只有在指定的配置选项下才被包含。
    • 配置选项 test:由 Rust 提供,用来编译和运行测试。
      • 只用 cargo test 才会编译代码,包括模块中的 helper 函数和 #[test] 标注的函数。

测试私用函数

  1. Rust 允许测试私有函数。

    pub fn add_two(a: i32) -> i32 {
        internal_adder(a, 2)
    }
    
    fn internal_adder(a: i32, b: i32) -> i32 {
        a + b
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn it_works() {
            assert_eq!(4, internal_adder(2, 2))
        }
    }
    

集成测试

  1. 在 Rust 中,集成测试完全位于被测试库的外部。
  2. 目的:是测试被测试库的多个部分是否能正确的一起工作。
  3. 集成测是覆盖率很重要。

tests 目录

  1. 创建集成测试:tests 目录。
    • tests 目录下的每一个测试文件都是单独的一个 crate。
      • 需要将被测试的库导入。
    • 无需标注 #[cfg(test)],tests 目录被特殊对待。
/*
.
├── Cargo.lock
├── Cargo.toml
├── src
│&nbsp;&nbsp; └── lib.rs
└── tests
    └── integration_test.rs
*/
use adder;
#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

运行指定的集成测试

  1. 运行一个特定的集成测试:cargo test 函数名称。

  2. 运行某个测试文件内的所有测试:cargo test –test 文件名。

  3. tests 目录下的每个文件被编译成单独的 crate:

    • 这些文件不共享行为(于 src 目录下规则不同)。
    • 如下例,本意的 common 只是工具方法,却別认为是一个单独的测试用例。
    Running tests/common.rs (target/debug/deps/common-36a0cc1efb478906)
    
    running 0 tests
    
    .
    ├── Cargo.lock
    ├── Cargo.toml
    ├── src
    │   └── lib.rs
    └── tests
        ├── common.rs
        └── integration_test.rs
    
    • 如下创建,mod 目录来存放工具方法,common 下不再被是被为单独的 crate:
     .
     ├── Cargo.lock
     ├── Cargo.toml
     ├── src
     │   └── lib.rs
     └── tests
         ├── common
         │   └── mod.rs
         └── integration_test.rs
    

针对 binary crate 的集成测试

  1. 如果项目是 binary crate,只含有 src/main.rs 没有 src/lib.rs:
    • 不能在 tests 目录下创建集成测试。
    • 无法把 main.rs 的函数导入作用域。
  2. 只用 library crate 才能暴露函数给其他 crate 用。
  3. binary crate 意味着独立运行。

文章作者: Layton
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Layton !
  目录