tdd-rust

Installation
SKILL.md

TDD Workflow for Rust Projects

Enforce strict Test-Driven Development: Red → Green → Refactor

Workflow Steps

1. RED: Write failing test first

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

    #[test]
    fn test_parse_session_metadata() {
        let path = Path::new("tests/fixtures/session.jsonl");
        let metadata = extract_metadata(path).unwrap();

        assert_eq!(metadata.message_count, 42);
        assert!(metadata.first_timestamp < metadata.last_timestamp);
    }
}

Run test to verify failure:

cargo test test_parse_session_metadata
# Expected: FAILED (function doesn't exist yet)

2. GREEN: Implement minimal code to pass

pub fn extract_metadata(path: &Path) -> anyhow::Result<SessionMetadata> {
    // Minimal implementation
    Ok(SessionMetadata {
        message_count: 42, // Hardcoded for first pass
        first_timestamp: Utc::now(),
        last_timestamp: Utc::now(),
        // ...
    })
}

Run test again:

cargo test test_parse_session_metadata
# Expected: PASSED

3. REFACTOR: Improve implementation

pub fn extract_metadata(path: &Path) -> anyhow::Result<SessionMetadata> {
    let file = File::open(path).context("Failed to open session file")?;
    let reader = BufReader::new(file);

    let mut first_line: Option<SessionLine> = None;
    let mut last_line: Option<SessionLine> = None;
    let mut count = 0;

    for line in reader.lines() {
        let line = line?;
        if let Ok(parsed) = serde_json::from_str::<SessionLine>(&line) {
            if first_line.is_none() {
                first_line = Some(parsed.clone());
            }
            last_line = Some(parsed);
            count += 1;
        }
    }

    Ok(SessionMetadata {
        message_count: count,
        first_timestamp: first_line.map(|l| l.timestamp).unwrap_or_else(Utc::now),
        last_timestamp: last_line.map(|l| l.timestamp).unwrap_or_else(Utc::now),
        // ...
    })
}

Final verification:

cargo test test_parse_session_metadata
cargo clippy --all-targets

Test Organization

Unit Tests (embedded in module)

// src/parsers/session_index.rs
pub fn extract_metadata(path: &Path) -> anyhow::Result<SessionMetadata> {
    // Implementation
}

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

    #[test]
    fn test_extract_metadata_valid_file() {
        // Test happy path
    }

    #[test]
    fn test_extract_metadata_malformed_lines() {
        // Test error handling
    }
}

Integration Tests (tests/ directory)

// tests/integration_test.rs
use ccboard_core::store::DataStore;

#[tokio::test]
async fn test_initial_load() {
    let store = DataStore::new();
    let report = store.initial_load().await.unwrap();
    assert!(report.stats_loaded);
}

Fixtures Management

Store sanitized test data in tests/fixtures/:

tests/
├─ fixtures/
│  ├─ stats-cache.json
│  ├─ session.jsonl
│  ├─ settings.json
│  └─ agent.md
└─ integration_test.rs

Load fixtures in tests:

#[test]
fn test_parse_stats() {
    let fixture = include_str!("../tests/fixtures/stats-cache.json");
    let stats: StatsCache = serde_json::from_str(fixture).unwrap();
    assert!(stats.total_input_tokens > 0);
}

Async Tests

use tokio::test;

#[tokio::test]
async fn test_parallel_session_scan() {
    let store = DataStore::new();

    // Test concurrent operations
    let tasks = vec![
        store.reload_stats(),
        store.update_session(path),
    ];

    let results = futures::future::join_all(tasks).await;
    // Assertions
}

Property-Based Testing (Optional)

# Cargo.toml
[dev-dependencies]
proptest = "1"
use proptest::prelude::*;

proptest! {
    #[test]
    fn test_session_parse_never_panics(s in "\\PC*") {
        // Should handle any input without panicking
        let _ = serde_json::from_str::<SessionLine>(&s);
    }
}

Pre-Commit Checklist

Before EVERY commit:

cargo fmt --all              # Format code
cargo clippy --all-targets   # Lint
cargo test --all             # All tests pass

Command Shortcuts

# Run specific test
cargo test test_name

# Run tests with output
cargo test -- --nocapture

# Run tests in module
cargo test parsers::

# Watch mode
cargo watch -x test

# Coverage (requires cargo-tarpaulin)
cargo tarpaulin --out Html

Anti-Patterns

DON'T skip the RED phase (test must fail first) ❌ DON'T write implementation before test ❌ DON'T use .unwrap() in tests - use .expect("meaningful message")DON'T commit without running tests

DO write test first, watch it fail ✅ DO implement minimal code to pass ✅ DO refactor after green ✅ DO use descriptive test names ✅ DO test both success and error paths

Related skills
Installs
3
GitHub Stars
37
First Seen
Mar 23, 2026