返回 Skill 列表
extension
分类: 开发与工程无需 API Key

rust-test-best-practice

全面的Rust自动化测试指南,涵盖单元测试、集成测试和文档测试。包括异步测试的实际模式、使用proptest进行基于属性的测试、使用trait进行模拟策略以及必要的测试工具(cargo-nextest、criterion、tarpaulin)。强调FIRST原则和CI/CD集成。在以下情况下使用:设置新的Rust项目、为团队建立测试标准、改进现有的测试套件、实施TDD工作流程或学习Rust测试生态系统的最佳实践。对于需要高可靠性和全面测试覆盖率的项目尤其有价值。

person作者: jakexiaohubgithub

Rust Automated Testing Best Practices Guide

1. Test Organization Structure

Unit Tests: Use #[cfg(test)] modules within the same file

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_basic_functionality() {
        assert_eq!(2 + 2, 4);
    }
}

Integration Tests: Place in tests/ directory to test public APIs

Documentation Tests: Embed examples in doc comments that double as tests

/// Adds two numbers together.
///
/// ```
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

2. Core Testing Principles

Use Descriptive Test Names: Clearly express test intent

#[test]
fn connection_timeout_returns_error_after_30_seconds() { }

Follow AAA Pattern: Arrange (setup), Act (execute), Assert (verify)

Test Boundary Conditions: Empty inputs, maximum values, error cases, edge scenarios

One Assertion Per Test: Focus each test on a single behavior (when practical)

3. Advanced Testing Techniques

Test Panics with #[should_panic]:

#[test]
#[should_panic(expected = "invalid input")]
fn test_panics_on_invalid_input() {
    process_data(""); // Should panic
}

Async Testing: Use tokio::test or async-std::test

#[tokio::test]
async fn test_async_operation() {
    let result = fetch_data().await;
    assert!(result.is_ok());
}

Property-Based Testing: Use proptest to test invariants across random inputs

proptest! {
    #[test]
    fn reversing_twice_returns_original(s: String) {
        let reversed_twice = s.chars().rev().collect::<String>()
            .chars().rev().collect::<String>();
        assert_eq!(s, reversed_twice);
    }
}

4. Mocking with mockall

Important: Before using mockall, always read the latest official documentation at https://docs.rs/mockall for up-to-date API and best practices.

When to Use Mocks

Mock objects serve two main purposes:

  1. Isolation Testing: When testing a specific module, mock the inputs and outputs of other modules to isolate the module under test.
  2. Simulating Edge Cases: Mock boundary conditions to test scenarios that are difficult to trigger or reproduce in real-world situations.

Key Principle: Mock the trait boundaries (interfaces/dependencies), not concrete implementations.

Basic Usage

Add to Cargo.toml:

[dev-dependencies]
mockall = "0.14"

Using #[automock] (from official repository):

#[cfg(test)]
use mockall::{automock, mock, predicate::*};

#[cfg_attr(test, automock)]
trait MyTrait {
    fn foo(&self, x: u32) -> u32;
}

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

    #[test]
    fn mytest() {
        let mut mock = MockMyTrait::new();
        mock.expect_foo()
            .with(eq(4))        // Argument matcher
            .times(1)           // Expected call count
            .returning(|x| x + 1);  // Return value
        
        assert_eq!(5, mock.foo(4));
    }
}

Return Values

#[automock]
trait MyTrait {
    fn foo(&self) -> u32;
    fn bar(&self, x: u32, y: u32) -> u32;
}

let mut mock = MockMyTrait::new();
mock.expect_foo()
    .return_const(42u32);       // Constant value
mock.expect_bar()
    .returning(|x, y| x + y);   // Computed value

Matching Multiple Calls

#[automock]
trait Foo {
    fn foo(&self, x: u32) -> u32;
}

let mut mock = MockFoo::new();
mock.expect_foo()
    .with(eq(5))
    .return_const(50u32);
mock.expect_foo()
    .with(eq(6))
    .return_const(60u32);

Sequences (Enforce Call Order)

#[automock]
trait Foo {
    fn foo(&self);
}

let mut seq = Sequence::new();

let mut mock1 = MockFoo::new();
mock1.expect_foo()
    .times(1)
    .in_sequence(&mut seq)
    .returning(|| ());

let mut mock2 = MockFoo::new();
mock2.expect_foo()
    .times(1)
    .in_sequence(&mut seq)
    .returning(|| ());

5. Essential Testing Tools

  • Cargo test: Built-in test runner with filtering via --test
  • Cargo-nextest: Faster parallel test execution with better output
  • Cargo-watch: Auto-run tests on file changes (cargo watch -x test)
  • Tarpaulin/llvm-cov: Code coverage analysis
  • Criterion: Statistical benchmarking framework

6. Test Execution Strategies

Run specific tests:

cargo test test_name
cargo test module_name::
cargo test --test integration_test_name

Run with output:

cargo test -- --nocapture  # Show println! output
cargo test -- --show-output  # Show output for passing tests

Parallel vs Sequential:

cargo test -- --test-threads=1  # Run serially

7. CI/CD Integration

Set up GitHub Actions to:

  • Run tests on multiple Rust versions (stable, beta, nightly)
  • Test all feature combinations: cargo test --all-features
  • Check code coverage and fail if below threshold
  • Run cargo clippy and cargo fmt --check

Example workflow snippet:

- name: Run tests
  run: |
    cargo test --all-features --workspace
    cargo test --doc

8. Performance and Benchmark Testing

Use criterion for reliable benchmarks:

use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn benchmark_function(c: &mut Criterion) {
    c.bench_function("my_function", |b| {
        b.iter(|| my_function(black_box(100)))
    });
}

criterion_group!(benches, benchmark_function);
criterion_main!(benches);

9. Test Quality Guidelines

Keep Tests FIRST:

  • Fast: Tests should run quickly
  • Isolated: No dependencies between tests
  • Repeatable: Same results every time
  • Self-validating: Pass or fail, no manual checking
  • Timely: Written alongside or before production code

Maintain Test Code Quality: Apply same standards as production code—tests should be clean, readable, and maintainable.

Avoid Test Flakiness: Don't use sleep(), random values without seeds, or depend on external state.

10. Common Patterns

Test Fixtures: Use setup() helper functions or the rstest crate for parameterized tests

Custom Assertions: Create helper functions for complex assertions

Error Testing: Test both error types and error messages

#[test]
fn returns_correct_error() {
    let result = fallible_operation();
    assert!(matches!(result, Err(Error::InvalidInput)));
}

Key Takeaway: Comprehensive testing is a cornerstone of reliable Rust software. Invest in your test suite—it pays dividends in confidence, maintainability, and rapid development.