测试策略
> 确保代码质量和功能正确性
测试类型
本项目包含三种测试:
1. 单元测试
测试单个函数和模块的功能。
位置: src/lib.rs 或各模块文件中的 #[cfg(test)] 模块
示例:
rust
▶ Run#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_todo() {
let mut todo = TodoList::new();
todo.add("Learn Rust".to_string());
assert_eq!(todo.len(), 1);
}
#[test]
fn test_complete_todo() {
let mut todo = TodoList::new();
let id = todo.add("Write code".to_string());
todo.complete(id).unwrap();
assert!(todo.get(id).unwrap().completed);
}
}运行单元测试:
bash
cargo test2. 集成测试
测试多个模块的协作和完整功能流程。
位置: tests/ 目录
示例: tests/integration_test.rs
rust
▶ Runuse todo_cli::{TodoList, Storage};
#[test]
fn test_full_workflow() {
// 创建临时存储
let temp_file = tempfile::NamedTempFile::new().unwrap();
let storage = Storage::new(temp_file.path());
// 创建待办列表
let mut todo = TodoList::new();
todo.add("Task 1".to_string());
todo.add("Task 2".to_string());
// 保存到存储
storage.save(&todo).unwrap();
// 从存储加载
let loaded = storage.load().unwrap();
assert_eq!(loaded.len(), 2);
}运行集成测试:
bash
cargo test --test integration_test3. 文档测试
测试文档中的代码示例。
位置: 文档注释 /// 中的代码块
示例:
rust
▶ Run/// 添加新的待办事项
///
/// # Examples
///
/// ```
/// use todo_cli::TodoList;
///
/// let mut todo = TodoList::new();
/// todo.add("Buy milk".to_string());
/// assert_eq!(todo.len(), 1);
/// ```
pub fn add(&mut self, text: String) -> usize {
// ...
}运行文档测试:
bash
cargo test --doc测试策略
测试金字塔
┌─────────┐
│ E2E │ 少量端到端测试
│ Tests │ (测试完整流程)
├─────────┤
│Integration│ 中量集成测试
│ Tests │ (测试模块协作)
├─────────┤
│ Unit │ 大量单元测试
│ Tests │ (测试单个函数)
└─────────┘测试覆盖范围
必须测试:
- ✅ 核心业务逻辑(添加、删除、完成待办)
- ✅ 错误处理(无效输入、文件操作失败)
- ✅ 边界条件(空列表、重复 ID)
- ✅ 数据持久化(保存、加载)
可选测试:
- ⚠️ CLI 参数解析(集成测试覆盖)
- ⚠️ 彩色输出(人工测试即可)
测试工具
测试框架
- 内置框架:
#[test]属性 - 断言宏:
assert!,assert_eq!,assert_ne! - 测试属性:
#[should_panic],#[ignore]
测试辅助库
toml
[dev-dependencies]
tempfile = "3.3" # 临时文件
assert_cmd = "2.0" # CLI 测试
predicates = "3.0" # 断言谓词示例: 使用 assert_cmd 测试 CLI
rust
▶ Runuse assert_cmd::Command;
#[test]
fn test_cli_add() {
let mut cmd = Command::cargo_bin("todo-cli").unwrap();
cmd.arg("add")
.arg("Buy groceries")
.assert()
.success()
.stdout(predicates::str::contains("Added"));
}测试场景
场景 1:添加待办事项
rust
▶ Run#[test]
fn test_add_todo_basic() {
let mut list = TodoList::new();
let id = list.add("Learn Rust".to_string());
assert_eq!(id, 0);
assert_eq!(list.len(), 1);
assert_eq!(list.get(id).unwrap().text, "Learn Rust");
}
#[test]
fn test_add_multiple_todos() {
let mut list = TodoList::new();
list.add("Task 1".to_string());
list.add("Task 2".to_string());
assert_eq!(list.len(), 2);
}场景 2:完成待办事项
rust
▶ Run#[test]
fn test_complete_todo() {
let mut list = TodoList::new();
let id = list.add("Write tests".to_string());
assert!(!list.get(id).unwrap().completed);
list.complete(id).unwrap();
assert!(list.get(id).unwrap().completed);
}
#[test]
fn test_complete_nonexistent_todo() {
let mut list = TodoList::new();
let result = list.complete(999);
assert!(result.is_err());
}场景 3:持久化存储
rust
▶ Run#[test]
fn test_save_and_load() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
let storage = Storage::new(temp_file.path());
let mut list = TodoList::new();
list.add("Task 1".to_string());
list.add("Task 2".to_string());
// 保存
storage.save(&list).unwrap();
// 加载
let loaded = storage.load().unwrap();
assert_eq!(loaded.len(), 2);
assert_eq!(loaded.get(0).unwrap().text, "Task 1");
}测试命令
运行所有测试
bash
cargo test运行特定测试
bash
# 运行单个测试
cargo test test_add_todo
# 运行匹配模式的测试
cargo test add
# 运行特定模块的测试
cargo test tests::todo_list显示测试输出
bash
# 显示 println! 输出
cargo test -- --nocapture
# 显示测试详细输出
cargo test -- --show-output并行测试
bash
# 单线程运行
cargo test -- --test-threads=1
# 指定线程数
cargo test -- --test-threads=4测试覆盖率
bash
# 安装 tarpaulin
cargo install cargo-tarpaulin
# 生成覆盖率报告
cargo tarpaulin --out Html测试最佳实践
1. 测试命名
rust
▶ Run// ✅ 好的命名
#[test]
fn test_add_todo_increases_length() { }
#[test]
fn test_complete_nonexistent_todo_returns_error() { }
// ❌ 差的命名
#[test]
fn test_add() { }
#[test]
fn test_error() { }2. 测试结构
rust
▶ Run#[test]
fn test_complete_todo() {
// Arrange(准备)
let mut list = TodoList::new();
let id = list.add("Task".to_string());
// Act(执行)
let result = list.complete(id);
// Assert(断言)
assert!(result.is_ok());
assert!(list.get(id).unwrap().completed);
}3. 测试隔离
rust
▶ Run#[test]
fn test_with_temporary_file() {
// 使用临时文件,测试结束后自动删除
let temp_file = tempfile::NamedTempFile::new().unwrap();
let storage = Storage::new(temp_file.path());
// 测试代码...
}4. 测试错误情况
rust
▶ Run#[test]
fn test_invalid_id() {
let list = TodoList::new();
// 测试错误情况
assert!(list.get(999).is_none());
assert!(list.complete(999).is_err());
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_panic_on_invalid_access() {
let list = TodoList::new();
list.get(0).unwrap(); // 应该 panic
}测试检查清单
完成以下测试后,即可认为项目测试充分:
[ ] 单元测试
- [ ] TodoList::new()
- [ ] TodoList::add()
- [ ] TodoList::complete()
- [ ] TodoList::delete()
- [ ] TodoList::list()
- [ ] Storage::save()
- [ ] Storage::load()
[ ] 集成测试
- [ ] 完整工作流(添加→完成→保存→加载)
- [ ] CLI 参数解析
- [ ] 错误处理流程
[ ] 文档测试
- [ ] 公共 API 文档示例
- [ ] README 中的代码示例
[ ] 边界测试
- [ ] 空列表操作
- [ ] 无效 ID 处理
- [ ] 文件不存在处理
- [ ] 权限错误处理
持续集成
在 .github/workflows/ci.yml 中配置测试:
yaml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Run tests
run: cargo test --all-features
- name: Run clippy
run: cargo clippy -- -D warnings相关资源
更新日志:
- 2026-04-03: 创建测试策略文档