跳到主要内容

测试组织和最佳实践

良好的测试组织和遵循最佳实践能够提高测试的可维护性、可读性和有效性。本节将介绍如何组织测试代码和编写高质量的测试。

测试项目结构

推荐的项目结构

my_project/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── main.rs
│ ├── models/
│ │ ├── mod.rs
│ │ ├── user.rs
│ │ └── post.rs
│ ├── services/
│ │ ├── mod.rs
│ │ ├── user_service.rs
│ │ └── post_service.rs
│ └── utils/
│ ├── mod.rs
│ └── validation.rs
├── tests/
│ ├── common/
│ │ └── mod.rs
│ ├── integration_tests.rs
│ ├── api_tests.rs
│ └── performance_tests.rs
├── benches/
│ └── benchmarks.rs
└── examples/
└── basic_usage.rs

模块内测试组织

// src/models/user.rs
#[derive(Debug, Clone, PartialEq)]
pub struct User {
pub id: u32,
pub name: String,
pub email: String,
pub age: u32,
}

impl User {
pub fn new(id: u32, name: String, email: String, age: u32) -> Result<Self, String> {
if name.is_empty() {
return Err("姓名不能为空".to_string());
}
if !email.contains('@') {
return Err("邮箱格式不正确".to_string());
}
if age > 150 {
return Err("年龄不合理".to_string());
}

Ok(User { id, name, email, age })
}

pub fn is_adult(&self) -> bool {
self.age >= 18
}

pub fn update_email(&mut self, new_email: String) -> Result<(), String> {
if !new_email.contains('@') {
return Err("邮箱格式不正确".to_string());
}
self.email = new_email;
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

mod user_creation {
use super::*;

#[test]
fn test_valid_user_creation() {
let user = User::new(1, "Alice".to_string(), "alice@example.com".to_string(), 25);
assert!(user.is_ok());

let user = user.unwrap();
assert_eq!(user.id, 1);
assert_eq!(user.name, "Alice");
assert_eq!(user.email, "alice@example.com");
assert_eq!(user.age, 25);
}

#[test]
fn test_empty_name_error() {
let result = User::new(1, "".to_string(), "alice@example.com".to_string(), 25);
assert_eq!(result, Err("姓名不能为空".to_string()));
}

#[test]
fn test_invalid_email_error() {
let result = User::new(1, "Alice".to_string(), "invalid_email".to_string(), 25);
assert_eq!(result, Err("邮箱格式不正确".to_string()));
}

#[test]
fn test_invalid_age_error() {
let result = User::new(1, "Alice".to_string(), "alice@example.com".to_string(), 200);
assert_eq!(result, Err("年龄不合理".to_string()));
}
}

mod user_methods {
use super::*;

#[test]
fn test_is_adult() {
let adult = User::new(1, "Adult".to_string(), "adult@example.com".to_string(), 25).unwrap();
assert!(adult.is_adult());

let minor = User::new(2, "Minor".to_string(), "minor@example.com".to_string(), 16).unwrap();
assert!(!minor.is_adult());

let exactly_18 = User::new(3, "Eighteen".to_string(), "eighteen@example.com".to_string(), 18).unwrap();
assert!(exactly_18.is_adult());
}

#[test]
fn test_update_email_success() {
let mut user = User::new(1, "Alice".to_string(), "alice@example.com".to_string(), 25).unwrap();

let result = user.update_email("newalice@example.com".to_string());
assert!(result.is_ok());
assert_eq!(user.email, "newalice@example.com");
}

#[test]
fn test_update_email_error() {
let mut user = User::new(1, "Alice".to_string(), "alice@example.com".to_string(), 25).unwrap();

let result = user.update_email("invalid_email".to_string());
assert_eq!(result, Err("邮箱格式不正确".to_string()));
assert_eq!(user.email, "alice@example.com"); // 邮箱应该保持不变
}
}
}

测试辅助工具

测试数据构建器

// tests/common/mod.rs
use my_project::models::User;

pub struct UserBuilder {
id: u32,
name: String,
email: String,
age: u32,
}

impl UserBuilder {
pub fn new() -> Self {
UserBuilder {
id: 1,
name: "Test User".to_string(),
email: "test@example.com".to_string(),
age: 25,
}
}

pub fn with_id(mut self, id: u32) -> Self {
self.id = id;
self
}

pub fn with_name(mut self, name: &str) -> Self {
self.name = name.to_string();
self
}

pub fn with_email(mut self, email: &str) -> Self {
self.email = email.to_string();
self
}

pub fn with_age(mut self, age: u32) -> Self {
self.age = age;
self
}

pub fn build(self) -> User {
User::new(self.id, self.name, self.email, self.age).unwrap()
}

pub fn try_build(self) -> Result<User, String> {
User::new(self.id, self.name, self.email, self.age)
}
}

impl Default for UserBuilder {
fn default() -> Self {
Self::new()
}
}

// 测试数据工厂
pub struct TestDataFactory;

impl TestDataFactory {
pub fn create_valid_user() -> User {
UserBuilder::new().build()
}

pub fn create_adult_user() -> User {
UserBuilder::new()
.with_age(25)
.build()
}

pub fn create_minor_user() -> User {
UserBuilder::new()
.with_age(16)
.build()
}

pub fn create_users(count: usize) -> Vec<User> {
(1..=count)
.map(|i| {
UserBuilder::new()
.with_id(i as u32)
.with_name(&format!("User{}", i))
.with_email(&format!("user{}@example.com", i))
.build()
})
.collect()
}
}

测试断言辅助

// tests/common/mod.rs 继续
pub fn assert_user_equals(actual: &User, expected: &User) {
assert_eq!(actual.id, expected.id, "用户ID不匹配");
assert_eq!(actual.name, expected.name, "用户姓名不匹配");
assert_eq!(actual.email, expected.email, "用户邮箱不匹配");
assert_eq!(actual.age, expected.age, "用户年龄不匹配");
}

pub fn assert_user_has_properties(user: &User, id: u32, name: &str, email: &str, age: u32) {
assert_eq!(user.id, id, "用户ID不匹配");
assert_eq!(user.name, name, "用户姓名不匹配");
assert_eq!(user.email, email, "用户邮箱不匹配");
assert_eq!(user.age, age, "用户年龄不匹配");
}

// 自定义断言宏
#[macro_export]
macro_rules! assert_user {
($user:expr, id: $id:expr, name: $name:expr, email: $email:expr, age: $age:expr) => {
assert_eq!($user.id, $id, "用户ID不匹配");
assert_eq!($user.name, $name, "用户姓名不匹配");
assert_eq!($user.email, $email, "用户邮箱不匹配");
assert_eq!($user.age, $age, "用户年龄不匹配");
};
}

// 使用示例
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_with_builder() {
let user = UserBuilder::new()
.with_name("Alice")
.with_email("alice@example.com")
.with_age(30)
.build();

assert_user!(user, id: 1, name: "Alice", email: "alice@example.com", age: 30);
}

#[test]
fn test_with_factory() {
let users = TestDataFactory::create_users(3);
assert_eq!(users.len(), 3);

assert_user!(users[0], id: 1, name: "User1", email: "user1@example.com", age: 25);
assert_user!(users[1], id: 2, name: "User2", email: "user2@example.com", age: 25);
assert_user!(users[2], id: 3, name: "User3", email: "user3@example.com", age: 25);
}
}

参数化测试

使用宏实现参数化测试

// 参数化测试宏
macro_rules! parameterized_test {
($test_name:ident, $test_fn:ident, $($case:expr),+) => {
$(
paste::paste! {
#[test]
fn [<$test_name _ $case:snake>]() {
$test_fn($case);
}
}
)+
};
}

// 测试函数
fn test_is_even_case(input: i32) {
let expected = input % 2 == 0;
assert_eq!(is_even(input), expected);
}

fn is_even(n: i32) -> bool {
n % 2 == 0
}

// 生成参数化测试
parameterized_test!(test_is_even, test_is_even_case, 0, 1, 2, 3, 4, 5, -1, -2);

// 更复杂的参数化测试
#[derive(Debug)]
struct TestCase {
input: i32,
expected: i32,
}

fn test_square_case(case: TestCase) {
assert_eq!(square(case.input), case.expected, "square({}) 失败", case.input);
}

fn square(x: i32) -> i32 {
x * x
}

#[test]
fn test_square_cases() {
let test_cases = vec![
TestCase { input: 0, expected: 0 },
TestCase { input: 1, expected: 1 },
TestCase { input: 2, expected: 4 },
TestCase { input: -3, expected: 9 },
TestCase { input: 5, expected: 25 },
];

for case in test_cases {
test_square_case(case);
}
}

属性测试(Property Testing)

// Cargo.toml 添加依赖
// [dev-dependencies]
// proptest = "1.0"

use proptest::prelude::*;

// 属性:任何数的平方都是非负数
proptest! {
#[test]
fn test_square_is_non_negative(x in any::<i32>()) {
let result = square(x);
prop_assert!(result >= 0, "square({}) = {} 应该是非负数", x, result);
}
}

// 属性:加法的交换律
proptest! {
#[test]
fn test_addition_commutative(a in any::<i32>(), b in any::<i32>()) {
prop_assert_eq!(add(a, b), add(b, a));
}
}

// 属性:字符串反转两次等于原字符串
proptest! {
#[test]
fn test_reverse_twice_is_identity(s in ".*") {
let reversed_twice = reverse(&reverse(&s));
prop_assert_eq!(reversed_twice, s);
}
}

fn reverse(s: &str) -> String {
s.chars().rev().collect()
}

fn add(a: i32, b: i32) -> i32 {
a + b
}

测试配置和环境

条件编译测试

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[cfg(target_os = "linux")]
fn test_linux_specific() {
// 只在 Linux 上运行的测试
assert!(true);
}

#[test]
#[cfg(target_os = "windows")]
fn test_windows_specific() {
// 只在 Windows 上运行的测试
assert!(true);
}

#[test]
#[cfg(feature = "expensive_tests")]
fn test_expensive_operation() {
// 只在启用 expensive_tests 特性时运行
// cargo test --features expensive_tests
std::thread::sleep(std::time::Duration::from_secs(1));
assert!(true);
}

#[test]
#[ignore]
fn test_ignored_by_default() {
// 默认被忽略的测试
// cargo test -- --ignored
assert!(true);
}
}

环境变量测试

use std::env;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_with_env_var() {
env::set_var("TEST_VAR", "test_value");

let result = get_config_value("TEST_VAR");
assert_eq!(result, Some("test_value".to_string()));

env::remove_var("TEST_VAR");
}

#[test]
fn test_without_env_var() {
env::remove_var("NONEXISTENT_VAR");

let result = get_config_value("NONEXISTENT_VAR");
assert_eq!(result, None);
}
}

fn get_config_value(key: &str) -> Option<String> {
env::var(key).ok()
}

性能和基准测试

简单性能测试

use std::time::Instant;

#[cfg(test)]
mod performance_tests {
use super::*;

#[test]
fn test_performance_large_vector() {
let size = 1_000_000;
let data: Vec<i32> = (0..size).collect();

let start = Instant::now();
let sum: i32 = data.iter().sum();
let duration = start.elapsed();

println!("求和 {} 个元素耗时: {:?}", size, duration);
assert_eq!(sum, (size - 1) * size / 2);

// 性能断言(可选)
assert!(duration.as_millis() < 100, "性能测试失败:耗时过长");
}

#[test]
fn test_algorithm_comparison() {
let data = vec![5, 2, 8, 1, 9, 3, 7, 4, 6];

// 测试冒泡排序
let mut bubble_data = data.clone();
let start = Instant::now();
bubble_sort(&mut bubble_data);
let bubble_time = start.elapsed();

// 测试快速排序
let mut quick_data = data.clone();
let start = Instant::now();
quick_data.sort();
let quick_time = start.elapsed();

println!("冒泡排序耗时: {:?}", bubble_time);
println!("快速排序耗时: {:?}", quick_time);

assert_eq!(bubble_data, quick_data);
assert!(quick_time < bubble_time, "快速排序应该更快");
}
}

fn bubble_sort(arr: &mut [i32]) {
let len = arr.len();
for i in 0..len {
for j in 0..len - 1 - i {
if arr[j] > arr[j + 1] {
arr.swap(j, j + 1);
}
}
}
}

基准测试

// benches/benchmarks.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use my_project::{add, multiply, square};

fn benchmark_basic_operations(c: &mut Criterion) {
c.bench_function("add", |b| b.iter(|| add(black_box(2), black_box(3))));
c.bench_function("multiply", |b| b.iter(|| multiply(black_box(4), black_box(5))));
c.bench_function("square", |b| b.iter(|| square(black_box(10))));
}

fn benchmark_vector_operations(c: &mut Criterion) {
let data: Vec<i32> = (0..1000).collect();

c.bench_function("vector_sum", |b| {
b.iter(|| {
let sum: i32 = black_box(&data).iter().sum();
black_box(sum)
})
});

c.bench_function("vector_max", |b| {
b.iter(|| {
let max = black_box(&data).iter().max();
black_box(max)
})
});
}

criterion_group!(benches, benchmark_basic_operations, benchmark_vector_operations);
criterion_main!(benches);

测试最佳实践

1. 测试命名约定

#[cfg(test)]
mod tests {
use super::*;

// 好的测试名称:描述性强,说明测试的内容和期望
#[test]
fn test_user_creation_with_valid_data_should_succeed() {
// 测试实现
}

#[test]
fn test_user_creation_with_empty_name_should_return_error() {
// 测试实现
}

#[test]
fn test_user_update_email_with_invalid_format_should_fail() {
// 测试实现
}

// 避免的测试名称:过于简单,不够描述性
#[test]
fn test_user() {
// 不清楚测试什么
}

#[test]
fn test1() {
// 完全没有意义的名称
}
}

2. 测试独立性

#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;

// 不好的做法:测试之间有依赖
static mut GLOBAL_COUNTER: i32 = 0;

#[test]
fn bad_test_1() {
unsafe {
GLOBAL_COUNTER += 1;
assert_eq!(GLOBAL_COUNTER, 1);
}
}

#[test]
fn bad_test_2() {
unsafe {
GLOBAL_COUNTER += 1;
assert_eq!(GLOBAL_COUNTER, 2); // 这个测试依赖于 bad_test_1
}
}

// 好的做法:每个测试都是独立的
#[test]
fn good_test_1() {
let mut counter = 0;
counter += 1;
assert_eq!(counter, 1);
}

#[test]
fn good_test_2() {
let mut counter = 0;
counter += 2;
assert_eq!(counter, 2);
}
}

3. 测试覆盖率

# 安装 tarpaulin
cargo install cargo-tarpaulin

# 运行覆盖率测试
cargo tarpaulin --out Html

# 查看覆盖率报告
open tarpaulin-report.html

4. 持续集成配置

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true

- name: Run tests
run: cargo test --verbose

- name: Run integration tests
run: cargo test --tests

- name: Run doc tests
run: cargo test --doc

- name: Check code coverage
run: |
cargo install cargo-tarpaulin
cargo tarpaulin --ciserver github-ci --coveralls $COVERALLS_TOKEN

良好的测试组织和实践能够显著提高代码质量和开发效率。记住:测试不仅是为了发现错误,更是为了设计更好的 API 和确保代码的长期可维护性。