Claude Code Plugins

Community-maintained marketplace

Feedback

rust-testing-verification

@matthewharwood/engmanager.xyz
1
0

Comprehensive testing strategies for Rust including property-based testing with proptest, fuzz testing with cargo-fuzz, benchmark testing with Criterion, contract testing for traits, and Miri for undefined behavior detection. Use when writing tests, ensuring code correctness, detecting edge cases, or measuring performance.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name rust-testing-verification
description Comprehensive testing strategies for Rust including property-based testing with proptest, fuzz testing with cargo-fuzz, benchmark testing with Criterion, contract testing for traits, and Miri for undefined behavior detection. Use when writing tests, ensuring code correctness, detecting edge cases, or measuring performance.

Rust Testing & Verification

Production testing strategies for correctness and performance validation

Version Context

  • proptest: 1.x
  • Criterion: 0.5.x
  • cargo-fuzz: 0.11.x
  • cargo-miri: Latest nightly

When to Use This Skill

  • Writing comprehensive test suites
  • Finding edge cases automatically
  • Verifying trait implementations
  • Benchmarking performance
  • Detecting undefined behavior
  • Ensuring code correctness

Property-Based Testing

Basic Property Tests

use proptest::prelude::*;

proptest! {
    /// Property: Serialization round-trip preserves data
    #[test]
    fn user_serialization_roundtrip(user in any::<User>()) {
        let serialized = serde_json::to_string(&user)?;
        let deserialized: User = serde_json::from_str(&serialized)?;
        prop_assert_eq!(user, deserialized);
    }

    /// Property: Email validation accepts valid emails
    #[test]
    fn email_validation_accepts_valid_emails(
        email in r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
    ) {
        let result = Email::parse(&email);
        prop_assert!(result.is_ok());
    }

    /// Property: Vec operations maintain size invariants
    #[test]
    fn vec_push_increases_len(
        mut vec in prop::collection::vec(any::<i32>(), 0..100),
        value in any::<i32>()
    ) {
        let original_len = vec.len();
        vec.push(value);
        prop_assert_eq!(vec.len(), original_len + 1);
    }
}

Advanced Property Tests

proptest! {
    /// Property: Account balance invariants
    #[test]
    fn account_balance_invariants(
        initial_balance in 0u64..1_000_000,
        transactions in prop::collection::vec(
            prop::oneof![
                (1u64..10_000).prop_map(Transaction::Deposit),
                (1u64..10_000).prop_map(Transaction::Withdrawal),
            ],
            1..100
        )
    ) {
        let mut account = Account::new(initial_balance);
        let mut expected_balance = initial_balance;

        for transaction in transactions {
            match transaction {
                Transaction::Deposit(amount) => {
                    account.deposit(amount)?;
                    expected_balance += amount;
                }
                Transaction::Withdrawal(amount) => {
                    if account.balance() >= amount {
                        account.withdraw(amount)?;
                        expected_balance -= amount;
                    }
                }
            }

            // Invariant: balance must always be non-negative
            prop_assert!(account.balance() >= 0);
            prop_assert_eq!(account.balance(), expected_balance);
        }
    }

    /// Property: Sorted vector stays sorted after insertion
    #[test]
    fn sorted_insert_maintains_order(
        mut sorted_vec in prop::collection::vec(any::<i32>(), 0..100)
            .prop_map(|mut v| { v.sort(); v }),
        value in any::<i32>()
    ) {
        sorted_vec.insert(
            sorted_vec.binary_search(&value).unwrap_or_else(|i| i),
            value
        );

        // Verify still sorted
        for i in 1..sorted_vec.len() {
            prop_assert!(sorted_vec[i - 1] <= sorted_vec[i]);
        }
    }
}

Custom Generators

use proptest::strategy::{Strategy, BoxedStrategy};

/// Custom strategy for generating valid users
fn arb_user() -> BoxedStrategy<User> {
    (
        r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
        r"[A-Z][a-z]+ [A-Z][a-z]+",
        13u8..=120u8,
    )
        .prop_map(|(email, name, age)| {
            User {
                id: UserId::new(),
                email,
                name,
                age,
                created_at: chrono::Utc::now(),
            }
        })
        .boxed()
}

proptest! {
    #[test]
    fn test_with_valid_users(user in arb_user()) {
        // Test only runs with valid user data
        prop_assert!(user.age >= 13 && user.age <= 120);
        prop_assert!(user.email.contains('@'));
    }
}

Fuzz Testing

Basic Fuzz Target

// fuzz/fuzz_targets/parse_input.rs
#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    // Should never panic on arbitrary input
    if let Ok(s) = std::str::from_utf8(data) {
        let _ = parse_user_input(s);
    }
});

Structured Fuzzing

// fuzz/fuzz_targets/api_request.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use arbitrary::Arbitrary;

#[derive(Debug, Arbitrary)]
struct FuzzApiRequest {
    method: String,
    path: String,
    headers: Vec<(String, String)>,
    body: Vec<u8>,
}

fuzz_target!(|req: FuzzApiRequest| {
    // Test API handler doesn't panic on arbitrary inputs
    let _ = handle_request(
        req.method,
        req.path,
        req.headers,
        req.body,
    );
});

Regression Tests from Fuzzing

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

    /// Edge cases discovered by fuzzing
    #[test]
    fn test_known_edge_cases() {
        let edge_cases = vec![
            "",                    // Empty input
            "\0",                  // Null byte
            "🦀",                  // Unicode
            &"x".repeat(10_000),   // Large input
            "\n\r\t",              // Whitespace
            "{{{{",                // Unbalanced braces
        ];

        for case in edge_cases {
            // Should handle gracefully without panicking
            let result = parse_user_input(case);
            assert!(result.is_ok() || result.is_err());
        }
    }
}

Benchmark Testing

Basic Criterion Benchmarks

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

fn benchmark_user_operations(c: &mut Criterion) {
    let mut group = c.benchmark_group("user_operations");

    // Benchmark with different input sizes
    for size in [10, 100, 1000, 10000].iter() {
        group.bench_with_input(
            BenchmarkId::new("lookup", size),
            size,
            |b, &size| {
                let users = generate_test_users(size);
                b.iter(|| {
                    let id = &users[rand::random::<usize>() % users.len()].id;
                    black_box(lookup_user(black_box(id)))
                })
            },
        );
    }

    group.finish();
}

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

Advanced Benchmarking

use criterion::{Criterion, BenchmarkId, Throughput};
use std::time::Duration;

fn benchmark_serialization(c: &mut Criterion) {
    let mut group = c.benchmark_group("serialization");

    // Configure statistical parameters
    group.sample_size(100);
    group.measurement_time(Duration::from_secs(10));
    group.confidence_level(0.95);

    for size in [10, 100, 1000].iter() {
        let data = generate_data(*size);

        // Set throughput for bytes per second calculation
        group.throughput(Throughput::Bytes(data.len() as u64));

        group.bench_with_input(
            BenchmarkId::new("json", size),
            &data,
            |b, data| {
                b.iter(|| {
                    let serialized = serde_json::to_string(black_box(data)).unwrap();
                    black_box(serialized)
                })
            },
        );

        group.bench_with_input(
            BenchmarkId::new("bincode", size),
            &data,
            |b, data| {
                b.iter(|| {
                    let serialized = bincode::serialize(black_box(data)).unwrap();
                    black_box(serialized)
                })
            },
        );
    }

    group.finish();
}

Allocation Benchmarking

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

    #[test]
    fn test_zero_allocation_path() {
        let allocations_before = allocation_counter::current();

        // Critical path that should not allocate
        let result = process_request_zero_alloc(&input);

        let allocations_after = allocation_counter::current();
        let total_allocations = allocations_after - allocations_before;

        assert_eq!(
            total_allocations, 0,
            "Critical path allocated {} bytes",
            total_allocations
        );
    }
}

Contract Testing

Trait Contract Tests

use async_trait::async_trait;

#[async_trait]
pub trait UserRepository: Send + Sync {
    async fn get_user(&self, id: UserId) -> Result<User, RepositoryError>;
    async fn save_user(&self, user: &User) -> Result<(), RepositoryError>;
}

/// Contract tests that all implementations must satisfy
#[cfg(test)]
pub mod contract_tests {
    use super::*;

    pub async fn test_user_repository_contract<R: UserRepository>(repo: R) {
        // Test: Save and retrieve should be consistent
        let user = User::new("test@example.com".to_string(), "Test User".to_string());

        repo.save_user(&user).await.unwrap();
        let retrieved = repo.get_user(user.id).await.unwrap();

        assert_eq!(user.id, retrieved.id);
        assert_eq!(user.email, retrieved.email);
        assert_eq!(user.name, retrieved.name);
    }

    pub async fn test_user_repository_not_found<R: UserRepository>(repo: R) {
        // Test: Getting non-existent user should return error
        let non_existent_id = UserId::new();
        let result = repo.get_user(non_existent_id).await;

        assert!(matches!(result, Err(RepositoryError::NotFound)));
    }
}

/// Apply contract tests to concrete implementation
#[tokio::test]
async fn postgres_repository_satisfies_contract() {
    let repo = PostgresUserRepository::new(get_test_db().await);
    contract_tests::test_user_repository_contract(repo.clone()).await;
    contract_tests::test_user_repository_not_found(repo).await;
}

#[tokio::test]
async fn in_memory_repository_satisfies_contract() {
    let repo = InMemoryUserRepository::new();
    contract_tests::test_user_repository_contract(repo.clone()).await;
    contract_tests::test_user_repository_not_found(repo).await;
}

Miri for Undefined Behavior Detection

Using Miri

# Install Miri
rustup +nightly component add miri

# Run tests with Miri
cargo +nightly miri test

# Run specific test
cargo +nightly miri test test_concurrent_access

Miri-Compatible Tests

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

    #[test]
    fn test_safe_concurrent_access() {
        use std::sync::Arc;
        use std::thread;

        let counter = Arc::new(AtomicCounter::new());
        let mut handles = vec![];

        for _ in 0..10 {
            let counter_clone = counter.clone();
            handles.push(thread::spawn(move || {
                for _ in 0..100 {
                    counter_clone.increment();
                }
            }));
        }

        for handle in handles {
            handle.join().unwrap();
        }

        assert_eq!(counter.get(), 1000);
    }
}

Table-Driven Tests

Data-Driven Test Cases

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

    #[test]
    fn test_email_validation() {
        let test_cases = vec![
            ("test@example.com", true),
            ("user+tag@domain.co.uk", true),
            ("invalid.email", false),
            ("@example.com", false),
            ("user@", false),
            ("", false),
        ];

        for (input, expected_valid) in test_cases {
            let result = Email::parse(input);
            assert_eq!(
                result.is_ok(),
                expected_valid,
                "Email validation failed for: {}",
                input
            );
        }
    }

    #[test]
    fn test_status_code_mapping() {
        let test_cases = vec![
            (ApiError::ValidationError(_), StatusCode::BAD_REQUEST),
            (ApiError::Unauthorized, StatusCode::UNAUTHORIZED),
            (ApiError::Forbidden, StatusCode::FORBIDDEN),
            (ApiError::NotFound, StatusCode::NOT_FOUND),
            (ApiError::InternalError, StatusCode::INTERNAL_SERVER_ERROR),
        ];

        for (error, expected_status) in test_cases {
            let status = error.status_code();
            assert_eq!(
                status, expected_status,
                "Status code mismatch for error: {:?}",
                error
            );
        }
    }
}

Best Practices

  1. Use property tests for invariant checking
  2. Fuzz parsers and deserializers extensively
  3. Benchmark hot paths with Criterion
  4. Write contract tests for trait implementations
  5. Run Miri on unsafe code and concurrent code
  6. Table-driven tests for comprehensive coverage
  7. Regression tests for bugs found in production
  8. Integration tests with real dependencies (testcontainers)

Common Dependencies

[dev-dependencies]
proptest = "1"
criterion = { version = "0.5", features = ["html_reports"] }
testcontainers = "0.23"

[dependencies]
# For fuzz testing
arbitrary = { version = "1", optional = true, features = ["derive"] }

[features]
fuzzing = ["arbitrary"]

CI Integration

# Run all test suites in CI
- name: Unit tests
  run: cargo test --workspace

- name: Property tests
  run: cargo test --workspace -- --ignored proptest

- name: Miri (UB detection)
  run: |
    rustup component add miri
    cargo miri test

- name: Benchmarks (smoke test)
  run: cargo bench --no-run

- name: Fuzz (smoke test)
  run: |
    cargo install cargo-fuzz
    timeout 60s cargo fuzz run parse_input || true