11编写自动化测试

主要内容:

  • Rust 测试功能的机制,

  • 编写测试时会用到的注解和宏,

  • 运行测试的默认行为和选项,

  • 如何将测试组织成单元测试和集成测试

单元测试

Unit test

单元测试所在位置

tests mod 中,该 mod 会有一个 #[cfg(test)] 属性注解,

单元测试则是标记了 #[test] 属性的函数/方法。

编写测试

测试函数,带有test 属性注解的函数。

fn 上面加上 #[test] 属性

执行 cargo test 命令运行测试时,Rust 会构建一个测试执行程序,调用标记了 test 属性的函数。

使用 cargo 新建库项目时,它会自动为我们生成一个测试模板和一个测试函数。

每一个测试都在一个新线程中运行,当主线程发现测试线程异常了,就将对应测试标记为失败。

  • 直接调用panic!宏,是最简单的造成 panic 的方法。

使用 assert! 宏来检查结果

assert!宏由标准库提供,需要条件结果为 true,否则会调用panic!宏,让测试失败。

使用 assert_eq!assert_ne!宏来测试相等

分别比较两个值是相等还是不相等来进行断言。

assert_eq!assert_ne! 宏在底层分别使用了 ==!=

当断言失败时,这两个宏辉使用调式格式打印出其参数,所以,被比较的值必须要实现PartialEqDebug 两个 trait。

  • PartialEq 用于判断值是否相等;
  • Debug 用于打印值;

注意,因为 PartialEq 和 Debug 两个 trait 都是派生 trait,所以通常可以直接在结构体或者枚举上添加 #[derive(PartialEq,Debug)] 注解。

可以参考附录C “可派生 trait” 中更多关于可派生 trait 的信息。

自定义失败信息

可以向assert!、assert_eq!、assert_ne! 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息打印出来。

可以使用{}占位符

1
2
3
4
5
6
7
8
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`", result
);
}

使用 should_panic 检查 panic

检查代码是否按照期望处理错误,可以使用 should_panic

#[test] 属性下面

  • 添加一个 #[should_panic] 属性;
  • 或者 #[should_panic(expected = "xxx")] 属性,expected 参数,会匹配panic 时错误信息,匹配则成功,否则失败

当函数panic 时测试通过,没有panic 时则失败。

1
2
3
4
5
6
7
8
9
10
11
12
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}


#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}

Result<T,E> 用于测试

测试失败时除了发生 panic,也可以使用 Result<T,E>。这样被测试函数中可以使用?运算符返回 Err 错误(参考[09错误处理](09错误处理.md### 传播错误的简写:? 运算符) 章节的?运算符部分)

测试通过返回Ok(())Ok() 中返回类型为 ()即unit,返回空值。

1
2
3
4
5
6
7
8
9
10
11
#[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> 的测试使用 #[should_panic] 注解。

当使用 Result<T,E> 的时候,应该在测试失败时直接返回 Err 值。

运行测试

运行测试使用 cargo test 命令,

cargo run 一样,test 命令也会编译并运行生成的测试二进制文件。

默认情况下,cargo test

  • 并行的运行所有测试;
  • 截获测试运行过程中产生的输出,不展示过程输出,只关心测试结果;

可使用的命令行参数分两部分,

  1. 一部分传递个cargo test,
  2. 另一部分传递给生成的测试二进制文件,

使用 -- 分隔符分隔这两种参数。

格式如下:

1
2
3
4
5
6
7
cargo test [传递给cargo test 的参数] -- [传递给二进制测试文件的参数]

cargo test [OPTIONS] [TESTNAME] [-- <args>...]

ARGS:
<TESTNAME> If specified, only run tests containing this string in their names
<args>... Arguments for the test binary

使用帮助:

1
2
cargo test --help     # 查看哪些参数可以传递给 cargo test 
cargo test -- --help # 查看哪些参数可以传递给二进制测试文件

并行或连续的运行测试

--test-threads 参数传递给二进制测试文件,支持控制线程的数量

1
cargo test -- --test-threads=1  // 使用1个线程,即不使用并行机制

显示函数输出

默认情况下测试时不会输出调用函数中的正常输出内容,例如println!() 的输出,仅会在测试执行失败时输出。

--nocapture 参数传递给二进制测试文件,支持打印测试中的值。

1
cargo test -- --nocapture

指定名字运行部分测试

cargo test [要运行的测试全称或者部分]

  • 指定全名称就运行单个test
  • 指定模糊名称就运行所有匹配上的test

忽略某些测试

#[test] 属性下面添加 #[ignore] 属性,这样运行时会自动忽略有该属性的test

1
2
3
4
5
#[test]
#[ignore]
fn expensive_test() {
// 需要运行一个小时的代码
}

cargo test -- --ignored 命令可以运行有 #[ignored] 属性标记的test

文档测试

documentation testing

相关内容查看14章编写有用的文档注释这部分

因为文档注释采用 Markdown 的语法,所以除了常规内容,插入代码,可以使用一对三个反引号 来标记。

如果代码可能引起panic,那么在反引号后添加rust,should_panic 的内容。

1
2
3
4
/// ```rust,should_panic
/// // panics on division by zero
/// testing_rbe::documentation_testing_rbe::div(10, 0);
/// ```

上面的内容,在编译出来的文档中就会标记为红色感叹号。

注意,在文档注释中调用代码,需要使用绝对路径,如上面样例所示,否则编译时会报错,找不到函数/方法。

测试的组织结构

Rust 中测试主要分为两类:

  • 单元测试(unit tests)
    • Rust 支持测试 private 的接口、方法、函数
  • 集成测试(integration tests)
    • 集成测试主要测试库的对外暴露的内容;
    • 主要目的时测试库的多个部分是否能够一起正常工作;

单元测试

  • 单元测试与要测试的源代码放在同一个文件中,
  • 测试模块需要在 tests模块中,同时标注 #[cfg(test)] 属性
    • 这个属性告诉 Rust 只在执行 cargo test 时才编译和运行测试代码,其他任务时不处理测试代码;
    • 这样在只希望构建库的时候,可以节省编译时间,同时不包含测试代码,减少编译后文件的大小;

集成测试

集成测试相对于源码来说是单独目录( tests 目录,与 src 同级)(独立 crate)

  • 所以使用库时与使用其他模块的方式一样,
  • 它们测试时只能调用被测库中的公有 API;
  • tests 目录下得测试文件不需要 #[cfg(test)] 属性标记;
1
2
3
4
5
6
7
8
foo
├── Cargo.toml
├── src
│ └── main.rs
│ └── lib.rs
└── tests
├── my_test.rs
└── my_other_test.rs

可以在 tests 目录下创建任意多个测试文件。

1
2
3
4
5
6
7
8
9
10
文件名:tests/integration_test.rs
内容:

use adder; // 引入 adder mod

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

指定集成测试文件运行测试

cargo test --test <集成测试文件名>

1
cargo test --test integration_test   //  测试文件 tests/integration_test.rs

集成测试中的子模块

tests 目录中的所有文件都会被编译为单独的 crate。

那么如何在集成测试目录下创建可以被多个测试调用的公共函数?

  • Rust 中 tests 目录下得子目录不会被作为单独的 crate 编译,不会被认为临时测试文件。
  • 所以,在tests 目录下创建子目录,然后创建单独文件即可。
1
将 setup 函数放到 tests/common/mod.rs 文件中
1
2
3
4
5
6
7
8
9
10
foo
├── Cargo.toml
├── src
│ └── main.rs
│ └── lib.rs
└── tests
├── my_test.rs
|-- common // 公共模块
\-- mod.rs
└── my_other_test.rs
1
2
3
4
5
6
7
8
9
use adder;

mod common;

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

二进制 crate 的集成测试

如果项目是二进制 crate 并且只包含 src/main.rs ,没有 src/lib.rs ,那么不能在tests目录创建集成测试。

  • 只有库 crate 才会向其他 crate 暴露可供调用和使用的函数
  • 二进制 crate 只关心单独运行。
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信