Rust TDD Command
This command enforces test-driven development methodology for Rust code using #[test], rstest, proptest, and mockall.
What This Command Does
- Define Types/Traits: Scaffold function signatures with
todo!() - Write Tests: Create comprehensive test module (RED)
- Run Tests: Verify tests fail for the right reason
- Implement Code: Write minimal code to pass (GREEN)
- Refactor: Improve while keeping tests green
- Check Coverage: Ensure 80%+ coverage with cargo-llvm-cov
When to Use
Use /rust-test when:
- Implementing new Rust functions, methods, or traits
- Adding test coverage to existing Rust code
- Fixing bugs (write failing test first)
- Building critical business logic
- Learning TDD workflow in Rust
TDD Cycle
RED -> Write failing test first
GREEN -> Implement minimal code to pass
REFACTOR -> Improve code, tests stay green
REPEAT -> Next test case
Example Session
User: /rust-test I need a function to validate user registration
Agent:
TDD Session: Registration Validator
Step 1: Define Interface
rust // src/validator.rs pub struct RegistrationRequest { pub name: String, pub email: String, pub password: String, }
pub enum ValidationResult {
Valid,
Invalid(Vec
pub fn validate_registration(request: &RegistrationRequest) -> ValidationResult { todo!() }
Step 2: Write Tests (RED)
rust
#[cfg(test)]
mod tests {
use super::*;#[test] fn valid_registration_returns_valid() { let request = RegistrationRequest { name: "Alice".into(), email: "alice@example.com".into(), password: "SecureP@ss1".into(), }; assert!(matches!(validate_registration(&request), ValidationResult::Valid)); }
#[test] fn blank_name_returns_invalid() { let request = RegistrationRequest { name: "".into(), email: "alice@example.com".into(), password: "SecureP@ss1".into(), }; match validate_registration(&request) { ValidationResult::Invalid(errors) => { assert!(errors.contains(&"Name is required".to_string())); } ValidationResult::Valid => panic!("expected Invalid"), } }
#[test] fn invalid_email_returns_invalid() { let request = RegistrationRequest { name: "Alice".into(), email: "not-an-email".into(), password: "SecureP@ss1".into(), }; assert!(matches!( validate_registration(&request), ValidationResult::Invalid(_) )); }
#[test] fn short_password_returns_invalid() { let request = RegistrationRequest { name: "Alice".into(), email: "alice@example.com".into(), password: "short".into(), }; assert!(matches!( validate_registration(&request), ValidationResult::Invalid(_) )); } }
Step 3: Run Tests — Verify FAIL
bash
$ cargo test
running 4 tests
test tests::valid_registration_returns_valid ... FAILED (not yet implemented)
test tests::blank_name_returns_invalid ... FAILED
test tests::invalid_email_returns_invalid ... FAILED
test tests::short_password_returns_invalid ... FAILED
Tests fail as expected (todo! panic).Step 4: Implement Minimal Code (GREEN)
rust
pub fn validate_registration(request: &RegistrationRequest) -> ValidationResult {
let mut errors = Vec::new();if request.name.trim().is_empty() { errors.push("Name is required".into()); } if !request.email.contains('@') { errors.push("Invalid email format".into()); } if request.password.len() < 8 { errors.push("Password must be at least 8 characters".into()); }
if errors.is_empty() { ValidationResult::Valid } else { ValidationResult::Invalid(errors) } }
Step 5: Run Tests — Verify PASS
bash
$ cargo test
running 4 tests
test tests::valid_registration_returns_valid ... ok
test tests::blank_name_returns_invalid ... ok
test tests::invalid_email_returns_invalid ... ok
test tests::short_password_returns_invalid ... oktest result: ok. 4 passed; 0 failed
All tests passing!Step 6: Check Coverage
bash
$ cargo llvm-cov
Coverage: 100.0% of lines
Coverage: 100%TDD Complete!
Test Patterns
Unit Tests
#[cfg(test)]
mod tests {
use super::*; #[test]
fn adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn handles_error() -> Result<(), Box<dyn std::error::Error>> {
let result = parse_config(r#"port = 8080"#)?;
assert_eq!(result.port, 8080);
Ok(())
}
}
Parameterized Tests with rstest
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);
}
Async Tests
#[tokio::test]
async fn fetches_data_successfully() {
let client = TestClient::new().await;
let result = client.get("/data").await;
assert!(result.is_ok());
}
Property-Based Tests
use proptest::prelude::*;proptest! {
#[test]
fn encode_decode_roundtrip(input in ".*") {
let encoded = encode(&input);
let decoded = decode(&encoded).unwrap();
assert_eq!(input, decoded);
}
}
Coverage Commands
# Summary report
cargo llvm-covHTML report
cargo llvm-cov --htmlFail if below threshold
cargo llvm-cov --fail-under-lines 80Run specific test
cargo test test_nameRun with output
cargo test -- --nocaptureRun without stopping on first failure
cargo test --no-fail-fast
Coverage Targets
| Code Type | Target | |-----------|--------| | Critical business logic | 100% | | Public API | 90%+ | | General code | 80%+ | | Generated / FFI bindings | Exclude |
TDD Best Practices
DO:
- Write test FIRST, before any implementation
- Run tests after each change
- Use
assert_eq!overassert!for better error messages - Use
?in tests that returnResultfor cleaner output - Test behavior, not implementation
- Include edge cases (empty, boundary, error paths)
- Write implementation before tests
- Skip the RED phase
- Use
#[should_panic]whenResult::is_err()works - Use
sleep()in tests — use channels ortokio::time::pause() - Mock everything — prefer integration tests when feasible
Related Commands
/rust-build- Fix build errors/rust-review- Review code after implementation/verify- Run full verification loop
Related
- Skill:
skills/rust-testing/ - Skill:
skills/rust-patterns/