Skip to main content
skill Claude Code CX Codex Cursor GitHub Copilot

rust-testing — Claude skill: Rust tests the TDD way

A skill for writing Rust tests: unit, integration, async, property-based, mocks and coverage. Useful when the project runs on TDD.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add affaan-m/ECC --skill rust-testing -a claude-code
Open on GitHub

What rust-testing does

The skill walks the RED-GREEN-REFACTOR loop: a failing test first, then the smallest implementation that passes, then cleanup. It ships patterns for #[cfg(test)] modules, the assert_eq!, assert_ne! and assert! macros, Result checks with matches!, and panic tests via #[should_panic].

Separate sections cover integration tests under tests/, async tests with #[tokio::test] and timeouts through tokio::time::timeout. Parameterized tests and fixtures use rstest; property-based tests use proptest with custom strategies. Dependencies get isolated with mockall, and coverage runs through cargo-llvm-cov with an 80%+ target.

Worth installing if you write Rust and want the agent to keep one test style instead of improvising per file. If the project has no tests and no plans for them, skip it.

What it helps with

  • write a failing test before the implementation
  • assert that a function returns the right error variant
  • test an async client and its timeouts
  • catch edge cases with property-based tests
  • swap a dependency for a mock in a unit test

Example prompts for the agent

  • write tests for this function TDD-style, failing test first
  • add a property-based roundtrip test for encode/decode
  • run coverage with cargo-llvm-cov and show uncovered lines

How to install rust-testing

1Skills CLI (Vercel)

npx skills add affaan-m/ECC --skill rust-testing -a claude-code

2Manually: put the files in place

~/.claude/skills/rust-testing/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/rust-testing/

From the same set146

The whole set →
video-editing affaan-m AI-assisted workflow for editing, trimming, and enriching real footage. Connects raw recordings with FFmpeg cuts, Remotion overlays, and ElevenLabs voiceovers. Claude Code CX Codex Cursor +1 6,500installs docker-patterns affaan-m A skill with Dockerfile and Docker Compose patterns: local dev stacks, networking, volumes, container hardening. Claude Code CX Codex Cursor +1 6,300installs frontend-patterns affaan-m A skill for React and Next.js work: component patterns, state, data fetching, and render performance. Use it when building or reviewing UI. Claude Code CX Codex Cursor +1 5,900installs backend-patterns affaan-m A skill with backend patterns for Node.js, Express and Next.js API routes: layering, caching, transactions, error handling. Claude Code CX Codex Cursor +1 5,900installs coding-standards affaan-m Baseline cross-project coding conventions for naming, immutability, and clean code principles. Useful for code reviews and refactoring. Claude Code CX Codex Cursor +1 5,600installs api-design affaan-m Provides REST API design standards covering resource naming, status codes, pagination, and error payloads. Use it when creating or auditing API contracts. Claude Code CX Codex Cursor +1 5,500installs laravel-security affaan-m This skill reviews Laravel authentication, Eloquent safety, CSRF, XSS, APIs, and production settings. Use it for security reviews or deployment checks. Claude Code CX Codex Cursor +1 5,400installs market-research affaan-m A skill for market research, competitor comparisons and investor diligence. Useful when you need a decision backed by evidence, not a tidy report. Claude Code CX Codex Cursor +1 5,400installs

Similar

web-perf cloudflare Audit Core Web Vitals, diagnose rendering bottlenecks, and inspect network dependencies directly through your agent. Claude Code CX Codex Cursor +1 109,300installs playwright-generate-test official github Generates a Playwright test from a scenario through Playwright MCP. Use it when the browser flow should be completed before test code is written. GitHub Copilot 18,162installs chrome-devtools-cli ChromeDevTools Lets AI coding agents interact with Chrome DevTools to automate clicks, inspect DOM snapshots, profile performance, and debug heap memory from the terminal. Claude Code CX Codex Cursor +1 6,700installs vitest Anthony Fu This skill guides work with Vitest tests, mocks, coverage, filtering, and fixtures. Use it when setting up tests or checking how Vitest fits a Vite project. Claude Code CX Codex Cursor +1 43,600installs improve-codebase-architecture Matt Pocock This skill scans a codebase for architectural friction, then builds a visual HTML report with refactoring candidates. Use it before a deeper architecture review. Claude Code 1,163,035installs design-taste-frontend Leonxlnx This skill directs the design of public pages and work screens, then checks the result in a browser through screenshots. Use it when an interface feels generic or unfinished. Claude Code CX Codex Cursor +1 673,100installs tdd Matt Pocock Guides the red-green TDD loop: ensures tests are written first, keep them at public interfaces, and avoid coupling to implementation details. Claude Code 1,141,062installs skill-creator official anthropics Creates and refines skills, then tests them with prompts, evaluations, and performance checks. Claude Code Claude.ai 414,180installs

Questions about rust-testing

How do I install rust-testing in Claude?

Run npx skills add affaan-m/ECC --skill rust-testing -a claude-code. The skill lands in ~/.claude/skills/rust-testing/SKILL.md.

What does rust-testing do?

It gives the agent Rust test patterns: unit, integration, async, property-based, mocks and coverage. The workflow follows TDD.

Which crates does the skill rely on?

rstest for parameterized tests, proptest for property-based ones, mockall for mocks, cargo-llvm-cov for coverage. Async tests use tokio.

Source text

SKILL.md · MIT
---
name: rust-testing
description: Rust testing patterns including unit tests, integration tests, async testing, property-based testing, mocking, and coverage. Follows TDD methodology. Use when writing Rust tests — unit, integration, async, property-based, or coverage.
metadata:
  origin: ECC
---

# Rust Testing Patterns

Comprehensive Rust testing patterns for writing reliable, maintainable tests following TDD methodology.

## When to Use

- Writing new Rust functions, methods, or traits
- Adding test coverage to existing code
- Creating benchmarks for performance-critical code
- Implementing property-based tests for input validation
- Following TDD workflow in Rust projects

## How It Works

1. **Identify target code** — Find the function, trait, or module to test
2. **Write a test** — Use `#[test]` in a `#[cfg(test)]` module, rstest for parameterized tests, or proptest for property-based tests
3. **Mock dependencies** — Use mockall to isolate the unit under test
4. **Run tests (RED)** — Verify the test fails with the expected error
5. **Implement (GREEN)** — Write minimal code to pass
6. **Refactor** — Improve while keeping tests green
7. **Check coverage** — Use cargo-llvm-cov, target 80%+

## TDD Workflow for Rust

### The RED-GREEN-REFACTOR Cycle

```
RED     → Write a failing test first
GREEN   → Write minimal code to pass the test
REFACTOR → Improve code while keeping tests green
REPEAT  → Continue with next requirement
```

### Step-by-Step TDD in Rust

```rust
// RED: Write test first, use todo!() as placeholder
pub fn add(a: i32, b: i32) -> i32 { todo!() }

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_add() { assert_eq!(add(2, 3), 5); }
}
// cargo test → panics at 'not yet implemented'
```

```rust
// GREEN: Replace todo!() with minimal implementation
pub fn add(a: i32, b: i32) -> i32 { a + b }
// cargo test → PASS, then REFACTOR while keeping tests green
```

## Unit Tests

### Module-Level Test Organization

```rust
// src/user.rs
pub struct User {
    pub name: String,
    pub email: String,
}

impl User {
    pub fn new(name: impl Into<String>, email: impl Into<String>) -> Result<Self, String> {
        let email = email.into();
        if !email.contains('@') {
            return Err(format!("invalid email: {email}"));
        }
        Ok(Self { name: name.into(), email })
    }

    pub fn display_name(&self) -> &str {
        &self.name
    }
}

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

    #[test]
    fn creates_user_with_valid_email() {
        let user = User::new("Alice", "[email protected]").unwrap();
        assert_eq!(user.display_name(), "Alice");
        assert_eq!(user.email, "[email protected]");
    }

    #[test]
    fn rejects_invalid_email() {
        let result = User::new("Bob", "not-an-email");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("invalid email"));
    }
}
```

### Assertion Macros

```rust
assert_eq!(2 + 2, 4);                                    // Equality
assert_ne!(2 + 2, 5);                                    // Inequality
assert!(vec![1, 2, 3].contains(&2));                     // Boolean
assert_eq!(value, 42, "expected 42 but got {value}");    // Custom message
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON);   // Float comparison
```

## Error and Panic Testing

### Testing `Result` Returns

```rust
#[test]
fn parse_returns_error_for_invalid_input() {
    let result = parse_config("}{invalid");
    assert!(result.is_err());

    // Assert specific error variant
    let err = result.unwrap_err();
    assert!(matches!(err, ConfigError::ParseError(_)));
}

#[test]
fn parse_succeeds_for_valid_input() -> Result<(), Box<dyn std::error::Error>> {
    let config = parse_config(r#"{"port": 8080}"#)?;
    assert_eq!(config.port, 8080);
    Ok(()) // Test fails if any ? returns Err
}
```

### Testing Panics

```rust
#[test]
#[should_panic]
fn panics_on_empty_input() {
    process(&[]);
}

#[test]
#[should_panic(expected = "index out of bounds")]
fn panics_with_specific_message() {
    let v: Vec<i32> = vec![];
    let _ = v[0];
}
```

## Integration Tests

### File Structure

```text
my_crate/
├── src/
│   └── lib.rs
├── tests/              # Integration tests
│   ├── api_test.rs     # Each file is a separate test binary
│   ├── db_test.rs
│   └── common/         # Shared test utilities
│       └── mod.rs
```

### Writing Integration Tests

```rust
// tests/api_test.rs
use my_crate::{App, Config};

#[test]
fn full_request_lifecycle() {
    let config = Config::test_default();
    let app = App::new(config);

    let response = app.handle_request("/health");
    assert_eq!(response.status, 200);
    assert_eq!(response.body, "OK");
}
```

## Async Tests

### With Tokio

```rust
#[tokio::test]
async fn fetches_data_successfully() {
    let client = TestClient::new().await;
    let result = client.get("/data").await;
    assert!(result.is_ok());
    assert_eq!(result.unwrap().items.len(), 3);
}

#[tokio::test]
async fn handles_timeout() {
    use std::time::Duration;
    let result = tokio::time::timeout(
        Duration::from_millis(100),
        slow_operation(),
    ).await;

    assert!(result.is_err(), "should have timed out");
}
```

## Test Organization Patterns

### Parameterized Tests with `rstest`

```rust
use rstest::{rstest, fixture};

#[rstest]
#[case("hello", 5)]
#[case("", 0)]
#[case("rust", 4)]
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
    assert_eq!(input.len(), expected);
}

// Fixtures
#[fixture]
fn test_db() -> TestDb {
    TestDb::new_in_memory()
}

#[rstest]
fn test_insert(test_db: TestDb) {
    test_db.insert("key", "value");
    assert_eq!(test_db.get("key"), Some("value".into()));
}
```

### Test Helpers

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

    /// Creates a test user with sensible defaults.
    fn make_user(name: &str) -> User {
        User::new(name, &format!("{name}@test.com")).unwrap()
    }

    #[test]
    fn user_display() {
        let user = make_user("alice");
        assert_eq!(user.display_name(), "alice");
    }
}
```

## Property-Based Testing with `proptest`

### Basic Property Tests

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

proptest! {
    #[test]
    fn encode_decode_roundtrip(input in ".*") {
        let encoded = encode(&input);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(input, decoded);
    }

    #[test]
    fn sort_preserves_length(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
        let original_len = vec.len();
        vec.sort();
        assert_eq!(vec.len(), original_len);
    }

    #[test]
    fn sort_produces_ordered_output(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
        vec.sort();
        for window in vec.windows(2) {
            assert!(window[0] <= window[1]);
        }
    }
}
```

### Custom Strategies

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

fn valid_email() -> impl Strategy<Value = String> {
    ("[a-z]{1,10}", "[a-z]{1,5}")
        .prop_map(|(user, domain)| format!("{user}@{domain}.com"))
}

proptest! {
    #[test]
    fn accepts_valid_emails(email in valid_email()) {
        assert!(User::new("Test", &email).is_ok());
    }
}
```

## Mocking with `mockall`

### Trait-Based Mocking

```rust
use mockall::{automock, predicate::eq};

#[automock]
trait UserRepository {
    fn find_by_id(&self, id: u64) -> Option<User>;
    fn save(&self, user: &User) -> Result<(), StorageError>;
}

#[test]
fn service_returns_user_when_found() {
    let mut mock = MockUserRepository::new();
    mock.expect_find_by_id()
        .with(eq(42))
        .times(1)
        .returning(|_| Some(User { id: 42, name: "Alice".into() }));

    let service = UserService::new(Box::new(mock));
    let user = service.get_user(42).unwrap();
    assert_eq!(user.name, "Alice");
}

#[test]
fn service_returns_none_when_not_found() {
    let mut mock = MockUserRepository::new();
    mock.expect_find_by_id()
        .returning(|_| None);

    let service = UserService::new(Box::new(mock));
    assert!(service.get_user(99).is_none());
}
```

## Doc Tests

### Executable Documentation

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

/// Parses a config string.
///
/// # Errors
///
/// Returns `Err` if the input is not valid TOML.
///
/// ```no_run
/// use my_crate::parse_config;
///
/// let config = parse_config(r#"port = 8080"#).unwrap();
/// assert_eq!(config.port, 8080);
/// ```
///
/// ```no_run
/// use my_crate::parse_config;
///
/// assert!(parse_config("}{invalid").is_err());
/// ```
pub fn parse_config(input: &str) -> Result<Config, ParseError> {
    todo!()
}
```

## Benchmarking with Criterion

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

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

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

fn fibonacci(n: u64) -> u64 {
    match n {
        0 | 1 => n,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn bench_fibonacci(c: &mut Criterion) {
    c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}

criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);
```

## Test Coverage

### Running Coverage

```bash
# Install: cargo install cargo-llvm-cov (or use taiki-e/install-action in CI)
cargo llvm-cov                    # Summary
cargo llvm-cov --html             # HTML report
cargo llvm-cov --lcov > lcov.info # LCOV format for CI
cargo llvm-cov --fail-under-lines 80  # Fail if below threshold
```

### Coverage Targets

| Code Type | Target |
|-----------|--------|
| Critical business logic | 100% |
| Public API | 90%+ |
| General code | 80%+ |
| Generated / FFI bindings | Exclude |

## Testing Commands

```bash
cargo test                        # Run all tests
cargo test -- --nocapture         # Show println output
cargo test test_name              # Run tests matching pattern
cargo test --lib                  # Unit tests only
cargo test --test api_test        # Integration tests only
cargo test --doc                  # Doc tests only
cargo test --no-fail-fast         # Don't stop on first failure
cargo test -- --ignored           # Run ignored tests
```

## Best Practices

**DO:**
- Write tests FIRST (TDD)
- Use `#[cfg(test)]` modules for unit tests
- Test behavior, not implementation
- Use descriptive test names that explain the scenario
- Prefer `assert_eq!` over `assert!` for better error messages
- Use `?` in tests that return `Result` for cleaner error output
- Keep tests independent — no shared mutable state

**DON'T:**
- Use `#[should_panic]` when you can test `Result::is_err()` instead
- Mock everything — prefer integration tests when feasible
- Ignore flaky tests — fix or quarantine them
- Use `sleep()` in tests — use channels, barriers, or `tokio::time::pause()`
- Skip error path testing

## CI Integration

```yaml
# GitHub Actions
test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: clippy, rustfmt

    - name: Check formatting
      run: cargo fmt --check

    - name: Clippy
      run: cargo clippy -- -D warnings

    - name: Run tests
      run: cargo test

    - uses: taiki-e/install-action@cargo-llvm-cov

    - name: Coverage
      run: cargo llvm-cov --fail-under-lines 80
```

**Remember**: Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date.

© affaan-m · MIT · affaan-m/ECC

See also

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor