rust-test
Enforce TDD workflow for Rust. Write tests first, then implement. Verify 80%+ coverage with cargo-llvm-cov.
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 firstGREEN -> Implement minimal code to passREFACTOR -> Improve code, tests stay greenREPEAT -> Next test caseExample Session
User: /rust-test I need a function to validate user registration
Agent:# TDD Session: Registration Validator
## Step 1: Define Interface
```rust// src/validator.rspub struct RegistrationRequest { pub name: String, pub email: String, pub password: String,}
pub enum ValidationResult { Valid, Invalid(Vec<String>),}
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 testrunning 4 teststest tests::valid_registration_returns_valid ... FAILED (not yet implemented)test tests::blank_name_returns_invalid ... FAILEDtest tests::invalid_email_returns_invalid ... FAILEDtest tests::short_password_returns_invalid ... FAILED```
Tests fail as expected (todo! panic).
## Step 4: Implement Minimal Code (GREEN)
```rustpub 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 testrunning 4 teststest tests::valid_registration_returns_valid ... oktest tests::blank_name_returns_invalid ... oktest tests::invalid_email_returns_invalid ... oktest tests::short_password_returns_invalid ... ok
test result: ok. 4 passed; 0 failed```
All tests passing!
## Step 6: Check Coverage
```bash$ cargo llvm-covCoverage: 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 reportcargo llvm-cov
# HTML reportcargo llvm-cov --html
# Fail if below thresholdcargo llvm-cov --fail-under-lines 80
# Run specific testcargo test test_name
# Run with outputcargo test -- --nocapture
# Run without stopping on first failurecargo test --no-fail-fastCoverage 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)
DON’T:
- 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 implementationverification-loopskill - Run full verification loop
Related
- Skill:
skills/rust-testing/ - Skill:
skills/rust-patterns/