Rust 提供了如下测试类型:
- 单元测试:Unit testing;
- 文档测试:Doc testing;
- 集成测试:Integration testing;
Cargo.toml 为 testing/exampels 提供了单独的构建时依赖配置:
[dev-dependencies]
pretty_assertions = "1"
单元测试 Unit testing:
- 使用
#[cfg(test)]
注解test module
,该 module 在 cargo build 时会被忽略(减少代码体积),而只有运行 cargo test 时才被编译和执行; - 使用
#[test]
注解要测试的函数,test 函数内部使用assert!()/assert_eq!()/assert_ne!()/Result
等来报告错误,assert 宏支持格式化字符串消息; - 单元测试的代码和源码在同一个文件或目录, 可以用来测试非 public 的接口;
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
a - b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_bad_add() {
assert_eq!(bad_add(1, 2), 3);
}
}
Rust 2018 版本的单元测试函数支持返回 Result 来报错, 故可以使用 ?:
fn sqrt(number: f64) -> Result<f64, String> {
if number >= 0.0 {
Ok(number.powf(0.5))
} else {
Err("negative floats don't have square roots".to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sqrt() -> Result<(), String> {
let x = 4.0;
assert_eq!(sqrt(x)?.powf(2.0), x);
Ok(())
}
}
单元测试也支持匹配 panic!() , 使用 expected 匹配 panic 消息字符串;
pub fn divide_non_zero_result(a: u32, b: u32) -> u32 {
if b == 0 {
panic!("Divide-by-zero error");
} else if a < b {
panic!("Divide result is zero");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divide() {
assert_eq!(divide_non_zero_result(10, 2), 5);
}
#[test]
#[should_panic]
fn test_any_panic() {
divide_non_zero_result(1, 0);
}
#[test]
#[should_panic(expected = "Divide result is zero")]
fn test_specific_panic() {
divide_non_zero_result(1, 10);
}
}
为 test 函数添加 #[ignore]
来忽略测试,但可以使用 cargo test -- --ignored
来只运行这些被忽略的测试。
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
#[test]
fn test_add_hundred() {
assert_eq!(add(100, 2), 102);
assert_eq!(add(2, 100), 102);
}
#[test]
#[ignore]
fn ignored_test() {
assert_eq!(add(0, 0), 0);
}
}
文档测试:在 Rust 的注释中(markdown 语法)嵌入嵌入代码块, 在编译和测试时会自动运行这些代码:
/// First line is a short summary describing function.
///
/// The next lines present detailed documentation. Code blocks start with
/// triple backquotes and have implicit `fn main()` inside
/// and `extern crate <cratename>`. Assume we're testing `doccomments` crate:
///
/// ```
/// let result = doccomments::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Usually doc comments may include sections "Examples", "Panics" and "Failures".
///
/// The next function divides two numbers.
///
/// # Examples
///
/// ```
/// let result = doccomments::div(10, 2);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// The function panics if the second argument is zero.
///
/// ```rustshould_panic
/// // panics on division by zero
/// doccomments::div(10, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Divide-by-zero error");
}
a / b
}
集成测试:
- 集成测试位于被测试 crate 外部,目的是测试被测试库的多个部分能否正确的一起工作,所以覆盖率很重要;
- 集成测试位于 tests 目录下,各文件对应一个单独的集成测试,可以包含子 module,只能测试 public 接口;
// common 是各集成测试可以复用的 module,这样可以把测试公共的接口拆分为公共 module
// tests/common/mod.rs:
pub fn setup() {
// some setup code, like creating required files/directories, starting servers, etc.
}
// tests/integration_test.rs importing common module.
mod common;
// 集成测试会被单独编译为 binary 并执行,故不再需要 #cfg[test] 注解。
#[test]
fn test_add() {
// using common code.
common::setup();
assert_eq!(adder::add(3, 2), 5);
}
使用 cargo test
命令运行测试:
- 默认使用
多线程并发运行
所有测试(单元+集成),同时也会运行文档注释中的代码块; - cargo test
会生成一个可执行程序
,所以为 cargo test 指定参数时,包含两部分:- cargo test 命令本身的参数:直接放到 test 命令后;
- 编程生成的 test 程序的参数:放到 – 分割符号后,如 cargo test – –ignored;
例如:
- 运行匹配关键字的特殊函数/module 名称(只能指定一个关键字):
cargo test KEYWORD
- 只运行指定文件中的集成测试:
cargo test --test integrated_test_file_name
- 单线程运行测试任务:
cargo test -- --test-threads=1
- test 默认捕获 println!() 输出,只有当 test 出错时才显示。指定
cargo test -- --nocapture
显示所有终端输出。 - 只运行被忽略的测试:
cargo test -- --ignored
$ cargo test
running 2 tests
test tests::test_bad_add ... FAILED
test tests::test_add ... ok
failures:
---- tests::test_bad_add stdout ----
thread 'tests::test_bad_add' panicked at 'assertion failed: `(left == right)`
left: `-1`,
right: `3`', src/lib.rs:21:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
tests::test_bad_add
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
$ cargo test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests doccomments
running 3 tests
test src/lib.rs - add (line 7) ... ok
test src/lib.rs - div (line 21) ... ok
test src/lib.rs - div (line 31) ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
如果项目只有 binary 而没有 lib,则不能运行集成测试,解决办法是将 binary 中的逻辑拆解到 lib 中。
对于 tokio 单元或集成测试,需要使用 #[tokio::test]
代替 #[test]
属性宏,前者会创建一个单线程的异步运行时来执行测试:
#[cfg(test)]
mod tests {
use crate::health_check;
#[tokio::test]
async fn health_check_succeeds() {
let response = health_check().await;
// This requires changing the return type of `health_check` from `impl Responder` to
// `HttpResponse` to compile You also need to import it with `use
// actix_web::HttpResponse`!
assert!(response.status().is_success())
}
}
参考: