Rust 单元测试详细教程
一、Rust 单元测试详细教程
1、基本概念
在 Rust 中,单元测试直接写在源代码文件中,通常位于 #[cfg(test)]
标记的模块内。测试函数需用 #[test]
属性标记。
// 被测试函数
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// 测试模块
#[cfg(test)]
mod tests {
use super::*; // 引入外部函数
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5); // 断言测试
}
}
2、核心组件
(1) 断言宏
assert!(expr)
:表达式为真则通过assert_eq!(left, right)
:验证相等assert_ne!(left, right)
:验证不等#[should_panic]
:测试预期 panic
#[test]
#[should_panic(expected = "除数不能为零")]
fn test_divide_by_zero() {
divide(10, 0); // 预期触发 panic
}
(2) 测试模块组织
#[cfg(test)]
mod tests {
// 测试私有函数
#[test]
fn test_private() {
assert_eq!(internal_logic(), 42);
}
// 子测试模块
mod edge_cases {
#[test]
fn test_negative() {
assert_eq!(add(-1, -1), -2);
}
}
}
3、 高级技巧
(1) 测试初始化
#[cfg(test)]
mod tests {
// 共享初始化代码
fn setup() -> String {
String::from("test_data")
}
#[test]
fn test_data_processing() {
let data = setup();
assert!(!data.is_empty());
}
}
(2) 忽略测试
#[test]
#[ignore = "性能测试暂不运行"]
fn heavy_computation_test() {
// 耗时操作
}
(3) 条件编译
#[test]
#[cfg(feature = "network")]
fn network_api_test() {
// 仅当启用 network 特性时编译
}
4、测试运行控制
(1) 运行特定测试
cargo test test_add # 运行单个测试
cargo test add # 运行名称包含"add"的测试
(2) 并行控制
cargo test -- --test-threads=1 # 单线程运行
(3) 显示输出
cargo test -- --nocapture # 显示 println! 输出
5、最佳实践
-
测试覆盖率:使用
tarpaulin
工具cargo tarpaulin --ignore-tests
-
测试私有函数:直接测试模块内部实现
-
BDD 风格:使用
speculoos
库use speculoos::*; #[test] fn test_list() { let list = vec![1, 2, 3]; assert_that(&list).has_length(3); }
-
Mock 对象:使用
mockall
库#[automock] trait DataSource { fn fetch(&self) -> u32; } #[test] fn test_mock() { let mut mock = MockDataSource::new(); mock.expect_fetch().returning(|| 42); assert_eq!(mock.fetch(), 42); }
6、添加自定义消息
在 Rust 测试中添加自定义消息可通过断言宏的额外参数实现,以下是具体方法和示例:
1、 assert!
宏添加消息
#[test]
fn test_basic() {
let result = 2 + 2;
assert!(result == 4, "加法计算错误,得到 {} 但期望 4", result);
}
当断言失败时输出:
thread 'test_basic' panicked at '加法计算错误,得到 5 但期望 4'
2、assert_eq!
宏添加消息
#[test]
fn test_equality() {
let actual = "hello".to_string();
let expected = "world";
assert_eq!(actual, expected, "字符串不匹配: 实际='{}', 期望='{}'", actual, expected);
}
失败时输出:
thread 'test_equality' panicked at '字符串不匹配: 实际='hello', 期望='world''
3、使用 format!
构建复杂消息
#[test]
fn test_complex() {
let vec = vec![1, 2, 3];
assert!(
vec.len() > 5,
"向量长度不足: 长度={}, 内容={:?}",
vec.len(),
vec
);
}
4、最佳实践
- 清晰说明:明确说明期望值和实际值
assert!(user.is_admin(), "用户 {} 无管理员权限", user.id)
- 包含关键数据:在消息中输出相关变量值
- 避免敏感信息:不要在消息中包含密码等敏感数据
- 使用格式规范:
assert_eq!(result, 3.14159, "精度不足: {:.5} vs {:.5}", result, 3.14159)
5、错误消息原理
Rust 的断言宏本质上是:
macro_rules! assert {
($cond:expr, $($msg:tt)*) => {
if !$cond {
panic!($($msg)*)
}
}
}
自定义消息通过 panic!
宏的格式化能力实现,支持所有标准格式语法。
7、常见问题解决
- 测试未编译:检查
#[cfg(test)]
是否遗漏 - 无法访问私有项:使用
use super::*;
引入父模块 - I/O 测试失败:使用
tempfile
库处理临时文件 - 异步测试:使用
#[tokio::test]
属性
二、代码示例
1、正常通过
测试源码:
pub struct Counter {
value: i32,
}
impl Counter {
pub fn new() -> Self {
Counter { value: 0 }
}
pub fn increment(&mut self) {
self.value += 1;
}
pub fn get(&self) -> i32 {
self.value
}
pub fn add(&mut self, n: i32) {
self.value += n;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter_initial() {
let c = Counter::new();
assert_eq!(c.get(), 0);
}
#[test]
fn test_counter_increment() {
let mut c = Counter::new();
c.increment();
assert_eq!(c.get(), 1);
}
#[test]
fn test_counter_add() {
let mut c = Counter::new();
c.add(5);
assert_eq!(c.get(), 5,"test add failed! please check your code");
}
#[test]
#[should_panic]
fn test_overflow() {
let mut c = Counter::new();
for _ in 0..(i32::MAX as u32 + 1) {
c.increment(); //
}
}
}
测试结果:
PS G:\Learning\Rust\unitTest> cargo test
Compiling unitTest v0.1.0 (G:\Learning\Rust\unitTest)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.31s
Running unittests src\main.rs (target\debug\deps\unitTest-cb66f5a5bbe86c70.exe)
running 4 tests
test tests::test_counter_add ... ok
test tests::test_counter_increment ... ok
test tests::test_counter_initial ... ok
test tests::test_overflow - should panic ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.99s
PS G:\Learning\Rust\unitTest>
2、异常代码
测试代码:
pub struct Counter {
value: i32,
}
impl Counter {
pub fn new() -> Self {
Counter { value: 0 }
}
pub fn increment(&mut self) {
self.value += 1;
}
pub fn get(&self) -> i32 {
self.value
}
pub fn add(&mut self, n: i32) {
self.value += n;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter_initial() {
let c = Counter::new();
assert_eq!(c.get(), 0);
}
#[test]
fn test_counter_increment() {
let mut c = Counter::new();
c.increment();
assert_eq!(c.get(), 1);
}
#[test]
fn test_counter_add() {
let mut c = Counter::new();
c.add(5);
assert_eq!(c.get(), 6,"test add failed! please check your code");
}
#[test]
#[should_panic]
fn test_overflow() {
let mut c = Counter::new();
for _ in 0..(i32::MAX as u32 + 1) {
c.increment(); //
}
}
}
测试结果:
PS G:\Learning\Rust\unitTest> cargo test
Compiling unitTest v0.1.0 (G:\Learning\Rust\unitTest)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.31s
Running unittests src\main.rs (target\debug\deps\unitTest-cb66f5a5bbe86c70.exe)
running 4 tests
test tests::test_counter_add ... FAILED
test tests::test_counter_increment ... ok
test tests::test_counter_initial ... ok
test tests::test_overflow - should panic ... ok
failures:
---- tests::test_counter_add stdout ----
thread 'tests::test_counter_add' panicked at src\main.rs:42:9:
assertion `left == right` failed: test add failed! please check your code
left: 5
right: 6
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::test_counter_add
test result: FAILED. 3 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.83s
error: test failed, to rerun pass `--bin unitTest`
PS G:\Learning\Rust\unitTest>