Rust学习笔记 (十)编写自动化测试

#ProgrammingLanguage/Rust

How to Write Tests

编写测试代码一般要实现三个动作:

  1. 设置所需的输入数据或状态
  2. 运行你想要测试的代码段
  3. 判断结果是否是程序该有的预期

剖析测试功能

如果想将一个函数作为测试函数,非常简单,只需要在函数fn关键字之前,编写测试属性。注意属性是rust代码段的远数据。

在命令行执行 cargo test 命令,rust 就会编写带有测试属性的可执行文件,并且运行测试属性的函数,并返回每一个函数的测试结果。

在命令行运行

 cargo new adder --lib
     Created library `adder` project

进入到adder目录,查看src/lib.rs文件

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}
$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.57s
     Running target/debug/deps/adder-92948b65e88960b4

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

每个测试都在新线程中运行,并且当主线程看到测试线程已死亡时,该测试将标记为失败。

通过assert!宏来检查测试结果

assert!后面会跟着一个表达式,判断表达式的结果是是否为真,如果是真,assert!不做任何动作,反之会调用panic!,测试函数返回失败。

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }
}

fn main() {}

因为测试模块是内部模块,所以我们需要将外部模块中的测试代码带入内部模块的范围.我们在这里使用glob,因此我们在外部模块中定义的任何内容都可用于此测试模块。

请注意,我们在tests模块中添加了新行:use super :: * ;. 测试模块是一个常规模块,遵循我们在第7章[[在模块树中引用项目的路径]](https://doc.rust-lang.org/book /ch07-03-paths-for-referring-an-item-in-the-module-tree.html)部分。 由于tests模块是内部模块,因此我们需要将外部模块中的受测试代码纳入内部模块的范围。 我们在这里使用一个glob,因此我们在外部模块中定义的任何内容都可以用于该`“测试”模块。

如果断言失败,他们还将打印两个值,这使得更容易查看为什么测试失败;

当断言失败时,这些宏将使用调试格式打印其参数,这意味着要比较的值必须实现PartialEq和Debug特征。 所有原始类型和大多数标准库类型都实现了这些特征。

pub fn greeting(name: &str) -> String {
    format!("Hello {}!", name)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

我们只是断言输出包含输入参数的文本。

#[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"), //assert!() will judge the first express
            "Greeting did not contain name, value was `{}`",
            result
        );
    }

我们通过在测试函数中添加另一个属性should_panic来做到这一点。 如果函数内部的代码出现紧急情况,则此属性将通过测试; 如果函数内的代码没有引起恐慌,则测试将失败。

为了使should_panic的测试更加精确,我们可以在should_panic的属性中添加可选的expected参数。

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {}.",
                value
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "Guess value must be less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

编写测试以使它们返回Result <T,E>,使您能够在测试正文中使用问号运算符,这是编写测试的便捷方式,如果其中的任何操作返回“ Err”,则测试将会失败。

cargo test’‘生成的二进制文件的默认行为是并行运行所有测试并捕获测试运行期间生成的输出,从而阻止显示输出并使其更易于读取与测试结果相关的输出。

由于测试是同时运行的,因此请确保您的测试不相互依赖,也不依赖任何共享状态,包括共享环境,例如当前的工作目录或环境变量。

单线程测试

$ cargo test -- --test-threads=1

显示函数打印输出

$ cargo test -- --nocapture #old version
$ cargo test -- --show-output #new version

按名称运行测试子集

pub fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_two_and_two() {
        assert_eq!(4, add_two(2));
    }

    #[test]
    fn add_three_and_two() {
        assert_eq!(5, add_two(3));
    }

    #[test]
    fn one_hundred() {
        assert_eq!(102, add_two(100));
    }
}
$ cargo test one_hundred

筛选多个测试单元 因为上面的代码中,两个测试函数的名字中带有add

$ cargo test add 

运行带有add的测试单元。

条件性忽略测试单元

#[test]
fn it_works() {
    assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn expensive_test() {
    // code that takes an hour to run
}

fn main() {}

如果只想单独运行ignore测试单元

$ cargo test -- --ignored

Test Organization 单元测试和集成测试 集成测试完全在您的库的外部,并且以与其他任何外部代码相同的方式使用您的代码,仅使用公共接口,并且每个测试可能使用多个模块。

一般在每一个模块的源文件中,添加单元测试代码。 使用惯例是,在每一个功能模块的源文件里,创建一个tests模块,包含测试功能,并用 cfg(test) 注释模块。

The Tests Module and cfg(test)

#[cfg(test)]

表明注释单元的测试代码,只有在运行 cargo test 的时候执行。

Test Private Functions

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 internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}

fn main() {}

集成测试 集成测试一般都是在你自己的库的外部,调用自己写好的整体模块的对外公共API。 创建集成测试,首先需要一个tests文件夹。

在项目顶层目录,创建一个tests文件夹,和src在统一层级的目录下。 然后,我们可以在此目录中创建任意数量的测试文件,而Cargo会将每个文件作为单独的库进行编译。

在tests文件夹下的每一个测试都是一个单独的crate,所以需要用引入自己的代码库到测试文件里面。

每一个tests文件夹下的测试代码,都不洗药添加cfg[test]注释。

._src_lib.sc ⚠️项目名称是add_two并且创建的是lib不是binary

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 internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}

fn main() {}

._test_integration_test.rs ⚠️第一行引入adds_two也就是我们自己的库

use adds_two;

#[test]
fn it_adds_two() {
    assert_eq!(4, adds_two::add_two(2));
}

❯ cargo test
warning: function is never used: `main`
  --> src/lib.rs:19:4
   |
19 | fn main() {}
   |    ^^^^
   |
   = note: `#[warn(dead_code)]` on by default

    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running target/debug/deps/adds_two-8f4a5fc32f3523e7

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/integration_test-7b53fd21e969af6f

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests adds_two

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out