Test Design Document: asset-light
版本: 1.0.0
日期: 2025-12-20
测试工程师: TEA Agent
1. 测试概述
1.1 测试目标
为 asset-light MVP 建立全面的测试策略,确保:
- 核心业务逻辑正确性
- 数据持久化可靠性
- UI 交互符合预期
- 性能指标达标
1.2 测试范围
| 模块 | 测试类型 | 优先级 |
|---|---|---|
| 数据模型 | 单元测试 | P0 |
| 业务服务 | 单元测试 + 集成测试 | P0 |
| 数据库层 | 集成测试 | P0 |
| 组件渲染 | 组件测试 | P1 |
| 端到端流程 | E2E 测试 | P1 |
| 性能基准 | 性能测试 | P2 |
1.3 测试工具
| 用途 | 工具 |
|---|---|
| 单元测试 | Rust 内置 #[test] |
| 断言增强 | pretty_assertions |
| Mock | mockall |
| 异步测试 | tokio::test |
| 测试覆盖率 | cargo-tarpaulin |
| 基准测试 | criterion |
2. 测试策略
2.1 测试金字塔
┌───────────┐
│ E2E │ 少量关键流程
│ Tests │
├───────────┤
│Integration│ 服务 + 数据库
│ Tests │
├───────────┤
│ Unit │ 大量覆盖
│ Tests │ 模型 + 服务 + 工具
└───────────┘
2.2 覆盖率目标
| 层级 | 目标覆盖率 |
|---|---|
| 模型层 (models) | ≥ 90% |
| 服务层 (services) | ≥ 85% |
| 数据库层 (db) | ≥ 80% |
| 工具函数 (utils) | ≥ 90% |
| 整体 | ≥ 80% |
3. 单元测试
3.1 模型层测试
3.1.1 Asset 模型
测试文件: src/models/asset.rs
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use rust_decimal_macros::dec; #[test] fn test_asset_new() { let asset = Asset::new( "余额宝".to_string(), Category::Cash, dec!(10000.00), ); assert!(!asset.id.is_nil()); assert_eq!(asset.name, "余额宝"); assert_eq!(asset.category, Category::Cash); assert_eq!(asset.current_value, dec!(10000.00)); assert!(asset.sub_category.is_none()); assert!(asset.notes.is_none()); } #[test] fn test_category_display_name() { assert_eq!(Category::Cash.display_name(), "现金类"); assert_eq!(Category::Stable.display_name(), "稳健类"); assert_eq!(Category::Advanced.display_name(), "进阶类"); } #[test] fn test_asset_with_sub_category() { let mut asset = Asset::new( "沪深300ETF".to_string(), Category::Advanced, dec!(50000.00), ); asset.sub_category = Some("宽基指数".to_string()); assert_eq!(asset.sub_category, Some("宽基指数".to_string())); } } }
3.1.2 Snapshot 模型
测试文件: src/models/snapshot.rs
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use rust_decimal_macros::dec; #[test] fn test_snapshot_new_calculates_total() { let items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "资产A".to_string(), category: Category::Cash, sub_category: None, value: dec!(10000.00), }, SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "资产B".to_string(), category: Category::Stable, sub_category: None, value: dec!(20000.00), }, ]; let snapshot = Snapshot::new(items); assert_eq!(snapshot.total_value, dec!(30000.00)); assert_eq!(snapshot.items.len(), 2); } #[test] fn test_snapshot_category_total() { let items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "现金A".to_string(), category: Category::Cash, sub_category: None, value: dec!(10000.00), }, SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "现金B".to_string(), category: Category::Cash, sub_category: None, value: dec!(5000.00), }, SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "稳健".to_string(), category: Category::Stable, sub_category: None, value: dec!(20000.00), }, ]; let snapshot = Snapshot::new(items); assert_eq!(snapshot.category_total(&Category::Cash), dec!(15000.00)); assert_eq!(snapshot.category_total(&Category::Stable), dec!(20000.00)); assert_eq!(snapshot.category_total(&Category::Advanced), dec!(0.00)); } #[test] fn test_snapshot_empty_items() { let snapshot = Snapshot::new(vec![]); assert_eq!(snapshot.total_value, dec!(0.00)); assert!(snapshot.items.is_empty()); } } }
3.1.3 AllocationPlan 模型
测试文件: src/models/plan.rs
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use rust_decimal_macros::dec; #[test] fn test_default_balanced_plan() { let plan = AllocationPlan::default_balanced(); assert_eq!(plan.name, "平衡型"); assert!(plan.is_active); assert_eq!(plan.allocations.len(), 3); let total: Decimal = plan.allocations .iter() .map(|a| a.target_percentage) .sum(); assert_eq!(total, dec!(100)); } #[test] fn test_template_conservative() { let plan = AllocationPlan::template_conservative(); let cash = plan.allocations.iter() .find(|a| a.category == Category::Cash) .unwrap(); assert_eq!(cash.target_percentage, dec!(30)); } #[test] fn test_template_aggressive() { let plan = AllocationPlan::template_aggressive(); let advanced = plan.allocations.iter() .find(|a| a.category == Category::Advanced) .unwrap(); assert_eq!(advanced.target_percentage, dec!(70)); } } }
3.2 服务层测试
3.2.1 AnalysisService 测试
测试文件: src/services/analysis_service.rs
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use rust_decimal_macros::dec; fn create_test_assets() -> Vec<Asset> { vec![ Asset { id: Uuid::new_v4(), name: "现金".to_string(), category: Category::Cash, sub_category: None, current_value: dec!(25000.00), // 25% notes: None, created_at: Utc::now(), updated_at: Utc::now(), }, Asset { id: Uuid::new_v4(), name: "稳健".to_string(), category: Category::Stable, sub_category: None, current_value: dec!(40000.00), // 40% notes: None, created_at: Utc::now(), updated_at: Utc::now(), }, Asset { id: Uuid::new_v4(), name: "进阶".to_string(), category: Category::Advanced, sub_category: None, current_value: dec!(35000.00), // 35% notes: None, created_at: Utc::now(), updated_at: Utc::now(), }, ] } fn create_test_plan() -> AllocationPlan { AllocationPlan { id: Uuid::new_v4(), name: "测试方案".to_string(), description: None, is_active: true, allocations: vec![ Allocation { category: Category::Cash, target_percentage: dec!(20), min_percentage: None, max_percentage: None, }, Allocation { category: Category::Stable, target_percentage: dec!(40), min_percentage: None, max_percentage: None, }, Allocation { category: Category::Advanced, target_percentage: dec!(40), min_percentage: None, max_percentage: None, }, ], created_at: Utc::now(), updated_at: Utc::now(), } } #[test] fn test_calculate_deviation_normal() { let assets = create_test_assets(); let plan = create_test_plan(); let results = AnalysisService::calculate_deviation(&assets, &plan); assert_eq!(results.len(), 3); // 现金类: 25% - 20% = +5% (超配,轻度偏离) let cash = results.iter().find(|r| r.category == Category::Cash).unwrap(); assert_eq!(cash.deviation, dec!(5)); assert_eq!(cash.status, DeviationStatus::Mild); assert_eq!(cash.direction, DeviationDirection::Overweight); // 稳健类: 40% - 40% = 0% (正常) let stable = results.iter().find(|r| r.category == Category::Stable).unwrap(); assert_eq!(stable.deviation, dec!(0)); assert_eq!(stable.status, DeviationStatus::Normal); // 进阶类: 35% - 40% = -5% (低配,正常边界) let advanced = results.iter().find(|r| r.category == Category::Advanced).unwrap(); assert_eq!(advanced.deviation, dec!(-5)); assert_eq!(advanced.status, DeviationStatus::Normal); assert_eq!(advanced.direction, DeviationDirection::Underweight); } #[test] fn test_calculate_deviation_severe() { let assets = vec![ Asset { id: Uuid::new_v4(), name: "现金".to_string(), category: Category::Cash, sub_category: None, current_value: dec!(50000.00), // 50% notes: None, created_at: Utc::now(), updated_at: Utc::now(), }, Asset { id: Uuid::new_v4(), name: "进阶".to_string(), category: Category::Advanced, sub_category: None, current_value: dec!(50000.00), // 50% notes: None, created_at: Utc::now(), updated_at: Utc::now(), }, ]; let plan = create_test_plan(); let results = AnalysisService::calculate_deviation(&assets, &plan); // 现金类: 50% - 20% = +30% (严重偏离) let cash = results.iter().find(|r| r.category == Category::Cash).unwrap(); assert_eq!(cash.status, DeviationStatus::Severe); } #[test] fn test_calculate_deviation_empty_assets() { let assets: Vec<Asset> = vec![]; let plan = create_test_plan(); let results = AnalysisService::calculate_deviation(&assets, &plan); assert!(results.is_empty()); } #[test] fn test_calculate_return() { let start_items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "资产".to_string(), category: Category::Cash, sub_category: None, value: dec!(100000.00), }, ]; let end_items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "资产".to_string(), category: Category::Cash, sub_category: None, value: dec!(110000.00), }, ]; let start = Snapshot::new(start_items); let end = Snapshot::new(end_items); let result = AnalysisService::calculate_return(&start, &end); assert_eq!(result.absolute_return, dec!(10000.00)); assert_eq!(result.return_rate, dec!(10)); // 10% } #[test] fn test_calculate_return_negative() { let start_items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "资产".to_string(), category: Category::Advanced, sub_category: None, value: dec!(100000.00), }, ]; let end_items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "资产".to_string(), category: Category::Advanced, sub_category: None, value: dec!(90000.00), }, ]; let start = Snapshot::new(start_items); let end = Snapshot::new(end_items); let result = AnalysisService::calculate_return(&start, &end); assert_eq!(result.absolute_return, dec!(-10000.00)); assert_eq!(result.return_rate, dec!(-10)); // -10% } #[test] fn test_calculate_return_zero_start() { let start = Snapshot::new(vec![]); let end_items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "资产".to_string(), category: Category::Cash, sub_category: None, value: dec!(10000.00), }, ]; let end = Snapshot::new(end_items); let result = AnalysisService::calculate_return(&start, &end); assert_eq!(result.return_rate, dec!(0)); // 避免除零 } #[test] fn test_category_return_contribution() { let start_items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "现金".to_string(), category: Category::Cash, sub_category: None, value: dec!(50000.00), }, SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "进阶".to_string(), category: Category::Advanced, sub_category: None, value: dec!(50000.00), }, ]; let end_items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "现金".to_string(), category: Category::Cash, sub_category: None, value: dec!(51000.00), // +1000 }, SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "进阶".to_string(), category: Category::Advanced, sub_category: None, value: dec!(59000.00), // +9000 }, ]; let start = Snapshot::new(start_items); let end = Snapshot::new(end_items); let result = AnalysisService::calculate_return(&start, &end); // 总收益 10000,现金贡献 10%,进阶贡献 90% let cash_contrib = result.category_breakdown .iter() .find(|c| c.category == Category::Cash) .unwrap(); assert_eq!(cash_contrib.contribution_rate, dec!(10)); let advanced_contrib = result.category_breakdown .iter() .find(|c| c.category == Category::Advanced) .unwrap(); assert_eq!(advanced_contrib.contribution_rate, dec!(90)); } } }
3.3 工具函数测试
3.3.1 金额格式化
测试文件: src/utils/format.rs
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use rust_decimal_macros::dec; #[test] fn test_format_currency() { assert_eq!(format_currency(dec!(1234567.89)), "¥ 1,234,567.89"); assert_eq!(format_currency(dec!(0)), "¥ 0.00"); assert_eq!(format_currency(dec!(-1234.56)), "-¥ 1,234.56"); } #[test] fn test_format_percentage() { assert_eq!(format_percentage(dec!(25.5)), "25.50%"); assert_eq!(format_percentage(dec!(0)), "0.00%"); assert_eq!(format_percentage(dec!(-5.25)), "-5.25%"); } #[test] fn test_format_change() { assert_eq!(format_change(dec!(1234.56)), "+¥ 1,234.56"); assert_eq!(format_change(dec!(-1234.56)), "-¥ 1,234.56"); assert_eq!(format_change(dec!(0)), "¥ 0.00"); } #[test] fn test_format_change_percentage() { assert_eq!(format_change_percentage(dec!(5.25)), "+5.25%"); assert_eq!(format_change_percentage(dec!(-3.5)), "-3.50%"); } } }
3.3.2 日期处理
测试文件: src/utils/date.rs
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use chrono::NaiveDate; #[test] fn test_get_quarter() { assert_eq!(get_quarter(NaiveDate::from_ymd_opt(2025, 1, 15).unwrap()), 1); assert_eq!(get_quarter(NaiveDate::from_ymd_opt(2025, 4, 1).unwrap()), 2); assert_eq!(get_quarter(NaiveDate::from_ymd_opt(2025, 7, 31).unwrap()), 3); assert_eq!(get_quarter(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()), 4); } #[test] fn test_get_quarter_date_range() { let (start, end) = get_quarter_date_range(2025, 1); assert_eq!(start, NaiveDate::from_ymd_opt(2025, 1, 1).unwrap()); assert_eq!(end, NaiveDate::from_ymd_opt(2025, 3, 31).unwrap()); let (start, end) = get_quarter_date_range(2025, 4); assert_eq!(start, NaiveDate::from_ymd_opt(2025, 10, 1).unwrap()); assert_eq!(end, NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()); } #[test] fn test_format_date_display() { let date = NaiveDate::from_ymd_opt(2025, 12, 20).unwrap(); assert_eq!(format_date_display(date), "2025-12-20"); } #[test] fn test_format_date_friendly() { let date = NaiveDate::from_ymd_opt(2025, 12, 20).unwrap(); assert_eq!(format_date_friendly(date), "12月20日"); } } }
4. 集成测试
4.1 数据库层集成测试
测试文件: tests/db_integration.rs
#![allow(unused)] fn main() { use asset_light::db::{Database, AssetRepository, SnapshotRepository, PlanRepository}; use asset_light::models::*; use rust_decimal_macros::dec; use tempfile::TempDir; fn setup_test_db() -> (Database, TempDir) { let temp_dir = TempDir::new().unwrap(); let db_path = temp_dir.path().join("test.db"); std::env::set_var("ASSET_LIGHT_DB_PATH", db_path.to_str().unwrap()); let db = Database::new().unwrap(); db.run_migrations().unwrap(); (db, temp_dir) } #[test] fn test_asset_crud() { let (db, _temp) = setup_test_db(); let repo = AssetRepository::new(db.conn()); // Create let asset = Asset::new("测试资产".to_string(), Category::Cash, dec!(10000)); repo.insert(&asset).unwrap(); // Read let found = repo.find_by_id(asset.id).unwrap().unwrap(); assert_eq!(found.name, "测试资产"); // Update let mut updated = found.clone(); updated.current_value = dec!(20000); repo.update(&updated).unwrap(); let found = repo.find_by_id(asset.id).unwrap().unwrap(); assert_eq!(found.current_value, dec!(20000)); // Delete repo.delete(asset.id).unwrap(); let found = repo.find_by_id(asset.id).unwrap(); assert!(found.is_none()); } #[test] fn test_asset_find_by_category() { let (db, _temp) = setup_test_db(); let repo = AssetRepository::new(db.conn()); repo.insert(&Asset::new("现金1".to_string(), Category::Cash, dec!(1000))).unwrap(); repo.insert(&Asset::new("现金2".to_string(), Category::Cash, dec!(2000))).unwrap(); repo.insert(&Asset::new("稳健".to_string(), Category::Stable, dec!(3000))).unwrap(); let cash_assets = repo.find_by_category(Category::Cash).unwrap(); assert_eq!(cash_assets.len(), 2); let stable_assets = repo.find_by_category(Category::Stable).unwrap(); assert_eq!(stable_assets.len(), 1); } #[test] fn test_snapshot_with_items() { let (db, _temp) = setup_test_db(); let asset_repo = AssetRepository::new(db.conn()); let snapshot_repo = SnapshotRepository::new(db.conn()); // 创建资产 let asset = Asset::new("资产".to_string(), Category::Cash, dec!(10000)); asset_repo.insert(&asset).unwrap(); // 创建快照 let items = vec![SnapshotItem { asset_id: asset.id, asset_name: asset.name.clone(), category: asset.category.clone(), sub_category: None, value: asset.current_value, }]; let snapshot = Snapshot::new(items); snapshot_repo.insert(&snapshot).unwrap(); // 查询快照 let found = snapshot_repo.find_by_id(snapshot.id).unwrap().unwrap(); assert_eq!(found.items.len(), 1); assert_eq!(found.total_value, dec!(10000)); } #[test] fn test_plan_activation() { let (db, _temp) = setup_test_db(); let repo = PlanRepository::new(db.conn()); // 创建两个方案 let plan1 = AllocationPlan::default_balanced(); let mut plan2 = AllocationPlan::template_conservative(); plan2.is_active = false; repo.insert(&plan1).unwrap(); repo.insert(&plan2).unwrap(); // 激活 plan2 repo.set_active(plan2.id).unwrap(); // 验证 let found1 = repo.find_by_id(plan1.id).unwrap().unwrap(); let found2 = repo.find_by_id(plan2.id).unwrap().unwrap(); assert!(!found1.is_active); assert!(found2.is_active); // 查询激活方案 let active = repo.find_active().unwrap().unwrap(); assert_eq!(active.id, plan2.id); } #[test] fn test_snapshot_daily_limit() { let (db, _temp) = setup_test_db(); let repo = SnapshotRepository::new(db.conn()); // 创建 5 个快照 for i in 0..5 { let snapshot = Snapshot::new(vec![]); repo.insert(&snapshot).unwrap(); } // 第 6 个应该检查失败 let count = repo.count_today().unwrap(); assert_eq!(count, 5); } }
5. 业务流程测试
5.1 盘点流程测试
#![allow(unused)] fn main() { #[cfg(test)] mod inventory_flow_tests { use super::*; #[test] fn test_complete_inventory_flow() { let (db, _temp) = setup_test_db(); let asset_service = AssetService::new(db.clone()); let snapshot_service = SnapshotService::new(db.clone()); // 1. 创建资产 let asset1 = Asset::new("资产1".to_string(), Category::Cash, dec!(10000)); let asset2 = Asset::new("资产2".to_string(), Category::Stable, dec!(20000)); asset_service.create(&asset1).unwrap(); asset_service.create(&asset2).unwrap(); // 2. 执行盘点 let assets = asset_service.list_all().unwrap(); let snapshot = snapshot_service.create_snapshot(&assets).unwrap(); assert_eq!(snapshot.total_value, dec!(30000)); assert_eq!(snapshot.items.len(), 2); // 3. 更新资产 let mut updated = asset1.clone(); updated.current_value = dec!(15000); asset_service.update(&updated).unwrap(); // 4. 再次盘点 let assets = asset_service.list_all().unwrap(); let snapshot2 = snapshot_service.create_snapshot(&assets).unwrap(); assert_eq!(snapshot2.total_value, dec!(35000)); // 5. 验证历史 let history = snapshot_service.list_all().unwrap(); assert_eq!(history.len(), 2); } #[test] fn test_inventory_daily_limit() { let (db, _temp) = setup_test_db(); let asset_service = AssetService::new(db.clone()); let snapshot_service = SnapshotService::new(db.clone()); // 创建资产 let asset = Asset::new("资产".to_string(), Category::Cash, dec!(10000)); asset_service.create(&asset).unwrap(); // 盘点 5 次 for _ in 0..5 { let assets = asset_service.list_all().unwrap(); snapshot_service.create_snapshot(&assets).unwrap(); } // 第 6 次应该失败 let assets = asset_service.list_all().unwrap(); let result = snapshot_service.create_snapshot(&assets); assert!(result.is_err()); match result { Err(AppError::LimitExceeded(msg)) => { assert!(msg.contains("5次")); } _ => panic!("Expected LimitExceeded error"), } } } }
5.2 收益分析流程测试
#![allow(unused)] fn main() { #[cfg(test)] mod analysis_flow_tests { use super::*; #[test] fn test_quarterly_return_analysis() { let (db, _temp) = setup_test_db(); let snapshot_repo = SnapshotRepository::new(db.conn()); // 创建 Q4 的两个快照 let start_items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "资产".to_string(), category: Category::Cash, sub_category: None, value: dec!(100000), }, ]; let mut start = Snapshot::new(start_items); start.snapshot_date = NaiveDate::from_ymd_opt(2025, 10, 1).unwrap(); snapshot_repo.insert(&start).unwrap(); let end_items = vec![ SnapshotItem { asset_id: Uuid::new_v4(), asset_name: "资产".to_string(), category: Category::Cash, sub_category: None, value: dec!(105000), }, ]; let mut end = Snapshot::new(end_items); end.snapshot_date = NaiveDate::from_ymd_opt(2025, 12, 20).unwrap(); snapshot_repo.insert(&end).unwrap(); // 查询 Q4 收益 let (q_start, q_end) = get_quarter_date_range(2025, 4); let snapshots = snapshot_repo.find_in_range(q_start, q_end).unwrap(); assert_eq!(snapshots.len(), 2); let result = AnalysisService::calculate_return( snapshots.last().unwrap(), // 最早 snapshots.first().unwrap(), // 最新 ); assert_eq!(result.absolute_return, dec!(5000)); assert_eq!(result.return_rate, dec!(5)); } } }
6. 边界条件测试
6.1 数据边界
#![allow(unused)] fn main() { #[cfg(test)] mod boundary_tests { use super::*; #[test] fn test_asset_name_max_length() { let name = "a".repeat(100); let asset = Asset::new(name.clone(), Category::Cash, dec!(1000)); assert_eq!(asset.name.len(), 100); // 超过 100 字符应该验证失败 let long_name = "a".repeat(101); let result = validate_asset_name(&long_name); assert!(result.is_err()); } #[test] fn test_asset_value_zero() { let asset = Asset::new("零值资产".to_string(), Category::Cash, dec!(0)); assert_eq!(asset.current_value, dec!(0)); } #[test] fn test_asset_value_negative_rejected() { let result = validate_asset_value(dec!(-100)); assert!(result.is_err()); } #[test] fn test_asset_value_precision() { // 保留 2 位小数 let asset = Asset::new("资产".to_string(), Category::Cash, dec!(1234.567)); // 应该被截断或四舍五入到 1234.57 assert_eq!(asset.current_value.scale(), 2); } #[test] fn test_allocation_total_not_100() { let plan = AllocationPlan { id: Uuid::new_v4(), name: "错误方案".to_string(), description: None, is_active: false, allocations: vec![ Allocation { category: Category::Cash, target_percentage: dec!(30), min_percentage: None, max_percentage: None, }, Allocation { category: Category::Stable, target_percentage: dec!(30), min_percentage: None, max_percentage: None, }, // 缺少 Advanced,总和只有 60% ], created_at: Utc::now(), updated_at: Utc::now(), }; let result = validate_allocation_plan(&plan); assert!(result.is_err()); } #[test] fn test_empty_assets_deviation() { let assets: Vec<Asset> = vec![]; let plan = AllocationPlan::default_balanced(); let results = AnalysisService::calculate_deviation(&assets, &plan); assert!(results.is_empty()); } #[test] fn test_single_snapshot_return() { // 只有一个快照时无法计算收益 let snapshot = Snapshot::new(vec![]); // 应该返回特殊值或错误 // 具体取决于 API 设计 } } }
7. 性能测试
7.1 基准测试
文件: benches/performance.rs
#![allow(unused)] fn main() { use criterion::{black_box, criterion_group, criterion_main, Criterion}; use asset_light::services::AnalysisService; use asset_light::models::*; use rust_decimal_macros::dec; use uuid::Uuid; fn create_large_asset_list(count: usize) -> Vec<Asset> { (0..count) .map(|i| Asset { id: Uuid::new_v4(), name: format!("资产{}", i), category: match i % 3 { 0 => Category::Cash, 1 => Category::Stable, _ => Category::Advanced, }, sub_category: None, current_value: dec!(10000), notes: None, created_at: Utc::now(), updated_at: Utc::now(), }) .collect() } fn bench_deviation_calculation(c: &mut Criterion) { let assets = create_large_asset_list(100); let plan = AllocationPlan::default_balanced(); c.bench_function("calculate_deviation_100_assets", |b| { b.iter(|| { AnalysisService::calculate_deviation( black_box(&assets), black_box(&plan), ) }) }); } fn bench_return_calculation(c: &mut Criterion) { let items: Vec<SnapshotItem> = (0..100) .map(|i| SnapshotItem { asset_id: Uuid::new_v4(), asset_name: format!("资产{}", i), category: Category::Cash, sub_category: None, value: dec!(10000), }) .collect(); let start = Snapshot::new(items.clone()); let end = Snapshot::new(items); c.bench_function("calculate_return_100_items", |b| { b.iter(|| { AnalysisService::calculate_return( black_box(&start), black_box(&end), ) }) }); } criterion_group!(benches, bench_deviation_calculation, bench_return_calculation); criterion_main!(benches); }
7.2 性能指标
| 场景 | 目标 | 验收标准 |
|---|---|---|
| 应用启动 | < 2s | 冷启动到可交互 |
| 页面切换 | < 200ms | 点击到渲染完成 |
| 盘点保存 | < 500ms | 100 个资产 |
| 偏离计算 | < 50ms | 100 个资产 |
| 收益计算 | < 100ms | 1000 条快照 |
8. 测试执行
8.1 测试命令
# 运行所有测试
cargo test
# 运行单元测试
cargo test --lib
# 运行集成测试
cargo test --test '*'
# 运行特定模块测试
cargo test models::
cargo test services::
# 显示详细输出
cargo test -- --nocapture
# 运行性能基准
cargo bench
# 生成覆盖率报告
cargo tarpaulin --out Html
8.2 CI 集成
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Run tests
run: cargo test --all
- name: Run clippy
run: cargo clippy -- -D warnings
9. 测试清单
9.1 模型层测试清单
| 测试 | 状态 |
|---|---|
| Asset::new() 创建 | ⬜ |
| Category 枚举显示名 | ⬜ |
| Snapshot 总值计算 | ⬜ |
| Snapshot 类别汇总 | ⬜ |
| AllocationPlan 模板 | ⬜ |
9.2 服务层测试清单
| 测试 | 状态 |
|---|---|
| 偏离度正常计算 | ⬜ |
| 偏离度严重偏离 | ⬜ |
| 偏离度空资产 | ⬜ |
| 收益正向计算 | ⬜ |
| 收益负向计算 | ⬜ |
| 收益贡献度 | ⬜ |
| 每日盘点限制 | ⬜ |
9.3 集成测试清单
| 测试 | 状态 |
|---|---|
| 资产 CRUD | ⬜ |
| 快照存储 | ⬜ |
| 方案激活切换 | ⬜ |
| 完整盘点流程 | ⬜ |
| 季度收益分析 | ⬜ |
9.4 边界测试清单
| 测试 | 状态 |
|---|---|
| 名称最大长度 | ⬜ |
| 零值资产 | ⬜ |
| 负值拒绝 | ⬜ |
| 占比非 100% | ⬜ |
| 空资产偏离 | ⬜ |
修订历史
| 版本 | 日期 | 作者 | 变更说明 |
|---|---|---|---|
| 1.0.0 | 2025-12-20 | TEA Agent | 初始版本 |