rust-testing

Rust testing patterns — unit tests with mockall, integration tests with sqlx transactions, HTTP handler testing (axum), benchmarks (criterion), property tests (proptest), fuzzing, and CI with cargo-nextest.

8 stars

Best use case

rust-testing is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Rust testing patterns — unit tests with mockall, integration tests with sqlx transactions, HTTP handler testing (axum), benchmarks (criterion), property tests (proptest), fuzzing, and CI with cargo-nextest.

Teams using rust-testing should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/rust-testing/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/rust-testing/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/rust-testing/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How rust-testing Compares

Feature / Agentrust-testingStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Rust testing patterns — unit tests with mockall, integration tests with sqlx transactions, HTTP handler testing (axum), benchmarks (criterion), property tests (proptest), fuzzing, and CI with cargo-nextest.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Rust Testing Patterns

Comprehensive testing strategies for Rust applications following TDD methodology.

## When to Activate

- Writing unit tests with mock dependencies
- Testing database-dependent code with sqlx
- Testing axum HTTP handlers
- Setting up benchmarks with criterion
- Configuring cargo-nextest in CI/CD
- Using `mockall::automock` to generate mock implementations from trait definitions
- Writing property-based tests with `proptest` to verify invariants across randomly generated inputs
- Isolating integration tests using `#[sqlx::test]` for automatic per-test transaction rollback

## TDD in Rust

```
RED   → Write a failing #[test]
GREEN → Write minimal implementation
REFACTOR → Improve while keeping tests green
```

## Unit Tests (Co-located)

The idiomatic Rust approach: unit tests live in the same file, in a `#[cfg(test)]` module.

```rust
// src/domain/discount.rs
pub fn apply_discount(price: f64, tier: CustomerTier) -> f64 {
    match tier {
        CustomerTier::Standard => price,
        CustomerTier::Silver   => price * 0.95,
        CustomerTier::Gold     => price * 0.90,
        CustomerTier::Platinum => price * 0.80,
    }
}

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

    #[test]
    fn standard_tier_no_discount() {
        assert_eq!(apply_discount(100.0, CustomerTier::Standard), 100.0);
    }

    #[test]
    fn platinum_tier_20_percent_off() {
        assert_eq!(apply_discount(100.0, CustomerTier::Platinum), 80.0);
    }

    #[test]
    fn discount_rounds_correctly() {
        let result = apply_discount(33.33, CustomerTier::Gold);
        assert!((result - 30.0).abs() < 0.01);
    }
}
```

## Mocking with mockall

```toml
# Cargo.toml
[dev-dependencies]
mockall = "0.13"
```

```rust
// Define trait with #[automock] in production code
use mockall::automock;

#[automock]  // Generates MockUserRepository
#[async_trait::async_trait]
pub trait UserRepository: Send + Sync {
    async fn find_by_id(&self, id: i64) -> Result<Option<User>, DbError>;
    async fn save(&self, user: &NewUser) -> Result<User, DbError>;
    async fn delete(&self, id: i64) -> Result<(), DbError>;
}

// Test using the generated mock
#[cfg(test)]
mod tests {
    use super::*;
    use mockall::predicate::*;

    #[tokio::test]
    async fn register_user_saves_and_returns_user() {
        let mut mock = MockUserRepository::new();

        mock.expect_save()
            .with(predicate::function(|u: &NewUser| u.email == "alice@test.com"))
            .times(1)
            .returning(|u| Ok(User { id: 1, email: u.email.clone(), name: u.name.clone() }));

        let service = UserService::new(Arc::new(mock));
        let user = service.register("Alice", "alice@test.com").await.unwrap();

        assert_eq!(user.id, 1);
        assert_eq!(user.email, "alice@test.com");
    }

    #[tokio::test]
    async fn register_returns_error_on_db_failure() {
        let mut mock = MockUserRepository::new();

        mock.expect_save()
            .times(1)
            .returning(|_| Err(DbError::ConnectionFailed));

        let service = UserService::new(Arc::new(mock));
        let result = service.register("Alice", "alice@test.com").await;

        assert!(result.is_err());
    }
}
```

### mockall Predicates

```rust
use mockall::predicate::*;

// Exact value
.with(eq(42))
.with(eq("hello"))

// Custom predicate
.with(function(|x: &i32| *x > 0))

// String contains
.with(str::contains("@"))

// Multiple arguments
.with(eq(1), eq("name"))

// Any value (don't care)
.with(always())

// Call count
.times(1)            // exactly once
.times(2..=5)        // 2 to 5 times
.once()              // sugar for .times(1)
.never()             // must not be called
```

## Integration Tests with sqlx

```toml
# Cargo.toml
[dev-dependencies]
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio", "macros", "migrate"] }
tokio = { version = "1", features = ["full"] }
```

```rust
// tests/user_repository_test.rs
use sqlx::PgPool;

// Helper: create an isolated test transaction
async fn setup_db() -> PgPool {
    let url = std::env::var("TEST_DATABASE_URL")
        .expect("TEST_DATABASE_URL must be set for integration tests");
    let pool = PgPool::connect(&url).await.unwrap();
    sqlx::migrate!("./migrations").run(&pool).await.unwrap();
    pool
}

#[sqlx::test]   // sqlx::test handles setup/teardown with isolated transactions
async fn find_user_by_id_returns_none_when_not_found(pool: PgPool) {
    let repo = PostgresUserRepo::new(pool);
    let result = repo.find_by_id(9999).await.unwrap();
    assert!(result.is_none());
}

#[sqlx::test]
async fn save_and_find_by_id(pool: PgPool) {
    let repo = PostgresUserRepo::new(pool);

    let saved = repo.save(&NewUser {
        name: "Alice".to_string(),
        email: "alice@test.com".to_string(),
    }).await.unwrap();

    assert!(saved.id > 0);

    let found = repo.find_by_id(saved.id).await.unwrap();
    assert_eq!(found.unwrap().email, "alice@test.com");
}

#[sqlx::test]
async fn delete_removes_user(pool: PgPool) {
    let repo = PostgresUserRepo::new(pool);

    let user = repo.save(&NewUser { name: "Bob".to_string(), email: "b@test.com".to_string() })
        .await.unwrap();

    repo.delete(user.id).await.unwrap();

    let result = repo.find_by_id(user.id).await.unwrap();
    assert!(result.is_none());
}
```

## HTTP Handler Testing (axum)

```rust
// tests/user_api_test.rs
use axum::{
    body::Body,
    http::{Request, StatusCode},
};
use tower::ServiceExt;  // oneshot()
use serde_json::{json, Value};

fn test_app() -> axum::Router {
    let state = AppState {
        repo: Arc::new(InMemoryUserRepo::new()),
        config: Arc::new(Config::test()),
    };
    router(state)
}

#[tokio::test]
async fn get_user_returns_200() {
    let app = test_app();

    // Pre-seed data
    let create_resp = app.clone()
        .oneshot(
            Request::builder()
                .method("POST")
                .uri("/users")
                .header("content-type", "application/json")
                .body(Body::from(json!({"name": "Alice", "email": "a@test.com"}).to_string()))
                .unwrap()
        )
        .await
        .unwrap();
    assert_eq!(create_resp.status(), StatusCode::CREATED);
    let body: Value = serde_json::from_slice(
        &axum::body::to_bytes(create_resp.into_body(), usize::MAX).await.unwrap()
    ).unwrap();
    let user_id = body["id"].as_i64().unwrap();

    // Fetch
    let response = app
        .oneshot(
            Request::builder()
                .uri(format!("/users/{user_id}"))
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::OK);
    let body: Value = serde_json::from_slice(
        &axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()
    ).unwrap();
    assert_eq!(body["name"], "Alice");
}

#[tokio::test]
async fn get_user_returns_404_when_not_found() {
    let app = test_app();
    let response = app
        .oneshot(Request::builder().uri("/users/9999").body(Body::empty()).unwrap())
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
```

## Property-Based Tests (proptest)

```toml
[dev-dependencies]
proptest = "1"
```

```rust
use proptest::prelude::*;

proptest! {
    // Property: parse → serialize → parse is idempotent
    #[test]
    fn email_roundtrip(
        local in "[a-z]{1,20}",
        domain in "[a-z]{2,10}"
    ) {
        let raw = format!("{local}@{domain}.com");
        let email = Email::parse(&raw).unwrap();
        assert_eq!(email.as_str(), raw);
    }

    // Property: sorted is always ordered
    #[test]
    fn sort_is_ordered(mut values: Vec<i32>) {
        values.sort();
        for i in 1..values.len() {
            assert!(values[i-1] <= values[i]);
        }
    }

    // Property: discount never exceeds original price
    #[test]
    fn discount_never_exceeds_price(price in 0.01f64..1_000_000.0) {
        let discounted = apply_discount(price, CustomerTier::Platinum);
        assert!(discounted <= price);
        assert!(discounted >= 0.0);
    }
}
```

## Benchmarks (criterion)

```toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

[[bench]]
name = "my_bench"
harness = false
```

```rust
// benches/my_bench.rs
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};

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

    for size in [10, 100, 1000, 10_000].iter() {
        group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
            let data: Vec<i32> = (0..size).rev().collect();
            b.iter(|| {
                let mut v = data.clone();
                v.sort();
                black_box(v)
            });
        });
    }
    group.finish();
}

fn bench_string_format(c: &mut Criterion) {
    c.bench_function("format_email", |b| {
        b.iter(|| format!("{}@{}.com", black_box("alice"), black_box("example")))
    });
}

criterion_group!(benches, bench_sort, bench_string_format);
criterion_main!(benches);
```

```bash
# Run benchmarks
cargo bench

# Run specific benchmark
cargo bench -- bench_sort

# Save baseline
cargo bench -- --save-baseline before
# Make changes, then compare
cargo bench -- --baseline before
```

## Test Organization

```
src/
  lib.rs          # #[cfg(test)] mod tests { } — unit tests co-located
  domain/
    user.rs       # unit tests inside
    order.rs

tests/             # Integration tests — only use public API
  common/
    mod.rs        # Shared helpers: setup_db(), build_app()
  user_api.rs     # Full HTTP roundtrip tests
  user_repo.rs    # Repository integration tests

benches/           # criterion benchmarks
  throughput.rs
```

### Shared Test Helpers

```rust
// tests/common/mod.rs
use sqlx::PgPool;

pub async fn test_pool() -> PgPool {
    let url = std::env::var("TEST_DATABASE_URL").unwrap();
    let pool = PgPool::connect(&url).await.unwrap();
    sqlx::migrate!("./migrations").run(&pool).await.unwrap();
    pool
}

pub fn test_user() -> NewUser {
    NewUser {
        name: "Test User".to_string(),
        email: format!("test-{}@example.com", uuid::Uuid::new_v4()),
    }
}
```

## CLI Quick Reference

```bash
# Run all tests
cargo test

# Run specific test
cargo test test_name

# Run tests in a module
cargo test domain::

# Show println! output
cargo test -- --nocapture

# Run only ignored tests
cargo test -- --ignored

# Parallel test count
cargo test -- --test-threads=4

# Using cargo-nextest (much faster in CI)
cargo nextest run
cargo nextest run --test-threads=8

# With test coverage (llvm-cov)
cargo llvm-cov
cargo llvm-cov --html

# Fuzzing
cargo fuzz add fuzz_target_1
cargo fuzz run fuzz_target_1

# Benchmark
cargo bench
```

## CI/CD with cargo-nextest

```yaml
# .github/workflows/test.yml
- name: Install nextest
  uses: taiki-e/install-action@nextest

- name: Run tests
  run: cargo nextest run --profile ci

- name: Run benchmarks (verify compile)
  run: cargo bench --no-run
```

```toml
# .config/nextest.toml
[profile.ci]
fail-fast = false
test-threads = "num-cpus"
status-level = "fail"
```

## Quick Reference

| Scenario | Tool/Pattern |
|----------|-------------|
| Unit test | `#[test]` in `#[cfg(test)]` module |
| Async test | `#[tokio::test]` |
| Mock trait | `mockall::automock` |
| DB integration | `#[sqlx::test]` (isolated transaction) |
| HTTP handler | `axum` + `tower::ServiceExt::oneshot` |
| Property test | `proptest!` macro |
| Coverage | `cargo llvm-cov` |

For anti-patterns and common mistakes, see skill `rust-testing-advanced`.

Related Skills

zero-trust-patterns

8
from marvinrichter/clarc

Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.

visual-testing

8
from marvinrichter/clarc

Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration.

typescript-testing

8
from marvinrichter/clarc

TypeScript testing patterns: Vitest for unit/integration, Playwright for E2E, MSW for API mocking, Testing Library for React components. Core TDD methodology for TypeScript/JavaScript projects.

swift-testing

8
from marvinrichter/clarc

Swift testing patterns: Swift Testing framework (Swift 6+), XCTest for UI tests, async/await test cases, actor testing, Combine testing, and XCUITest for UI automation. TDD for Swift/SwiftUI.

swift-protocol-di-testing

8
from marvinrichter/clarc

Protocol-based dependency injection for testable Swift code — mock file system, network, and external APIs using focused protocols and Swift Testing.

scala-testing

8
from marvinrichter/clarc

Scala testing with ScalaTest, MUnit, and ScalaCheck: FunSpec/FlatSpec test structure, property-based testing with forAll, mocking with MockitoSugar, Cats Effect testing with munit-cats-effect (runTest/IOSuite), ZIO Test, Testcontainers-Scala for database integration tests, and CI integration with sbt. Use when writing or reviewing Scala tests.

rust-web-patterns

8
from marvinrichter/clarc

Axum HTTP handlers, Serde serialization, async channels, iterator patterns, trait objects, configuration, and WebAssembly target for Rust web services.

rust-testing-advanced

8
from marvinrichter/clarc

Advanced Rust testing anti-patterns and corrections — cfg(test) placement, expect() over unwrap(), mockall expectation ordering, executor mixing (#[tokio::test] vs block_on), PgPool isolation with

rust-patterns

8
from marvinrichter/clarc

Idiomatic Rust patterns, ownership idioms, async with Tokio, error handling with thiserror/anyhow, testing strategies, and hexagonal architecture in Rust.

rust-patterns-advanced

8
from marvinrichter/clarc

Advanced Rust patterns — zero-cost abstractions, proc macros, unsafe FFI, WASM, Axum web architecture, trait objects vs generics, and performance profiling.

ruby-testing

8
from marvinrichter/clarc

RSpec testing patterns for Ruby and Rails — factories, mocks, request specs, feature specs, VCR, and SimpleCov coverage.

r-testing

8
from marvinrichter/clarc

R testing patterns: testthat 3e with expect_* assertions, snapshot testing, mocking with mockery and httptest2, covr code coverage, lintr static analysis, property-based testing with hedgehog, testing Shiny apps with shinytest2. Use when writing or reviewing R tests.