Agent skill
Rust Testing Excellence
Write proper, clear tests that validate both valid and invalid inputs with explicit assertions
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/rust-testing-excellence
Metadata
Additional technical details for this skill
- author
- Main Agent
- version
- 3.1-approved
- last updated
- 2026-01-28
SKILL.md
Rust Testing Excellence
When to Use This Skill
Read this when writing or reviewing tests (not implementation or async code). This covers:
- Unit tests, integration tests, benchmarks
- Validating both valid AND invalid inputs
- Feature-gated test modules
- Property-based testing
- Avoiding false-positive tests
Do NOT read this for:
- Implementation → See rust-clean-implementation
- Async code → See rust-with-async-code
Core Testing Principles
The Three Test Validations ✅
Every meaningful test MUST validate:
- Input Validation - Verify inputs are handled correctly
- Output Verification - Confirm result matches expectations
- Error Path Testing - Ensure error conditions produce appropriate errors
// BAD ❌ - Creates variable with no assertions
#[test]
fn test_process() {
let result = process("valid_input").unwrap(); // Assumes success!
}
// GOOD ✅ - Validates both success and error paths
#[test]
fn test_process_valid_input() {
let result = process("valid_input");
assert!(result.is_ok(), "valid input should succeed");
assert_eq!(result.unwrap().len(), 11);
}
#[test]
fn test_process_invalid_input() {
let result = process("");
assert_eq!(result, Err(Error::EmptyInput));
}
Anti-Pattern: Muted Variables Without Assertions
CRITICAL: Tests that create variables but never validate their content are FORBIDDEN.
// BAD ❌ - False confidence, no validation
#[test]
fn test_user_creation() {
let user = User::new("Alice", "alice@example.com");
// Variable created but NEVER checked!
}
// GOOD ✅ - Explicit validation
#[test]
fn test_user_creation() {
let user = User::new("Alice", "alice@example.com")
.expect("should create user");
assert_eq!(user.name, "Alice");
assert_eq!(user.email(), "alice@example.com");
assert!(user.id() > 0);
}
Test Organization
Test Location Conventions
CRITICAL: Rust has specific conventions for where to place tests:
1. Unit Tests - Inside Source Files
// src/lib.rs or src/module.rs
pub fn public_function() -> Result<()> {
private_helper()
}
fn private_helper() -> Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_private_helper() {
// Unit tests can access private functions
assert!(private_helper().is_ok());
}
#[test]
fn test_public_function() {
assert!(public_function().is_ok());
}
}
2. Integration Tests - At Project Root
project_root/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/ # Integration tests at project root
├── crate_name/ # Organize by crate name
│ ├── api_tests.rs
│ ├── authentication.rs
│ └── error_handling.rs
└── common/ # Shared test utilities
└── mod.rs
// tests/crate_name/api_tests.rs
use my_crate::prelude::*; // Only public API
#[test]
fn test_full_workflow() {
let service = Service::new();
let result = service.process("input");
assert!(result.is_ok());
}
3. Benchmarks - At Project Root
project_root/
├── Cargo.toml
└── benches/ # Benchmarks at project root
└── crate_name/ # Organize by crate name
├── parsing.rs
└── serialization.rs
# Cargo.toml
[[bench]]
name = "crate_name_parsing"
harness = false
path = "benches/crate_name/parsing.rs"
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
Feature-Gated Tests
Use Module-Level Gates, Not Individual Attributes
MANDATORY: Group feature-specific tests into modules with #[cfg(...)] at module level.
// BAD ❌ - Individual test attributes scattered
#[cfg(test)]
mod tests {
#[test]
#[cfg(not(feature = "std"))]
fn test_spinlock_basic() { }
#[test]
#[cfg(not(feature = "std"))]
fn test_spinlock_contention() { }
#[test]
#[cfg(feature = "std")]
fn test_std_mutex() { }
}
// GOOD ✅ - Feature-gated test modules
#[cfg(test)]
mod tests {
use super::*;
// Common tests for all configurations
#[test]
fn test_basic_functionality() {
// Works with both std and no_std
}
// No-std specific tests grouped together
#[cfg(not(feature = "std"))]
mod nostd_tests {
use super::*;
#[test]
fn test_spinlock_basic() {
// SpinLock only available in no_std
}
#[test]
fn test_spinlock_contention() { }
}
// Std-specific tests grouped together
#[cfg(feature = "std")]
mod std_tests {
use super::*;
#[test]
fn test_std_mutex_threading() {
// Test with real OS threads
}
}
}
Benefits:
- ✅ Clear organization - see which tests run in which configuration
- ✅ Single location for feature changes
- ✅ Better test output and maintainability
- ✅ Feature-specific imports scoped to relevant module
Async Test Isolation
MANDATORY: See Async Test Isolation in rust-with-async-code for complete patterns.
Quick reference: Always use flavor = "current_thread" for async tests to prevent test isolation issues with global state.
Property-Based Testing (Recommended)
Use proptest to test invariants across hundreds of generated inputs automatically.
Why Property-Based Testing?
Property-based testing is highly recommended for:
- ✅ Testing invariants (properties that should always hold)
- ✅ Finding edge cases you didn't think of
- ✅ Serialization/deserialization roundtrips
- ✅ Parsers and data transformations
- ✅ Mathematical operations (commutativity, associativity, etc.)
- ✅ State machines and protocols
Basic Usage
use proptest::prelude::*;
proptest! {
#[test]
fn test_valid_inputs_produce_valid_outputs(
name in "[a-zA-Z]+",
value in 0i32..100,
) {
let user = User::new(&name);
prop_assert!(user.is_ok());
let result = process_value(user.unwrap(), value);
prop_assert!(result.is_ok());
prop_assert!(result.unwrap() >= 0);
}
#[test]
fn test_idempotency(input in "[a-zA-Z]+") {
let first = compute_hash(&input);
let second = compute_hash(&input);
prop_assert_eq!(first, second,
"Hash computation should be deterministic");
}
}
Common Property Testing Patterns
Roundtrip Properties (serialization):
proptest! {
#[test]
fn test_json_roundtrip(user in any::<User>()) {
let json = serde_json::to_string(&user)?;
let decoded: User = serde_json::from_str(&json)?;
prop_assert_eq!(user, decoded);
}
}
Invariant Properties (never panic):
proptest! {
#[test]
fn test_parser_never_panics(input in ".*") {
// Should never panic, regardless of input
let _ = parse_input(&input);
}
}
Relationship Properties (commutativity):
proptest! {
#[test]
fn test_addition_commutative(a in 0i32..1000, b in 0i32..1000) {
prop_assert_eq!(add(a, b), add(b, a));
}
}
When to Use Property-Based Testing
| Use Case | Example Property |
|---|---|
| Serialization | deserialize(serialize(x)) == x |
| Parsing | parse never panics on any input |
| Encoding | decode(encode(x)) == x |
| Sorting | Output is sorted and contains same elements |
| Hashing | hash(x) == hash(x) (deterministic) |
| Reversible operations | reverse(reverse(x)) == x |
Dependencies
Add to Cargo.toml:
[dev-dependencies]
proptest = "1.4"
Common Pitfalls
Pitfall 1: Testing Implementation Details
// BAD ❌ - Tests internal state
let internal_state = obj.get_internal_map();
assert_eq!(*internal_state.last_key(), "expected");
// GOOD ✅ - Tests observable behavior
obj.process("input");
assert!(obj.result().contains("output"));
Pitfall 2: No Error Path Testing
// BAD ❌ - Only tests success path
#[test]
fn test_valid_input() {
assert!(process(valid_data).is_ok());
}
// GOOD ✅ - Tests both paths
#[test]
fn test_valid_input() {
assert!(process(valid_data).is_ok());
}
#[test]
fn test_invalid_input() {
assert_eq!(process(""), Err(Error::EmptyInput));
assert_eq!(process(too_long), Err(Error::InputTooLong));
}
Pitfall 3: Missing Initialization in Tests
// BAD ❌ - Pool never initialized
#[test]
fn test_threaded_operation() {
let mut executor = ThreadPool::new(4);
// MISSING: No initialization call!
assert!(executor.is_ready());
}
// GOOD ✅ - Properly initializes and validates
#[test]
fn test_threaded_operation() {
let mut thread_pool = ExecutorPool::with_capacity(4);
// MANDATORY: Initialize before testing
assert!(thread_pool.initialize().is_ok(),
"Pool must initialize successfully");
// Now validate actual behavior
assert!(thread_pool.is_ready());
}
Test Helper Functions
Create reusable helpers for common test setup:
#[cfg(test)]
mod tests {
use super::*;
fn create_test_user(name: &str) -> User {
User::new(name, format!("{}@test.com", name))
.expect("should create test user")
}
fn assert_valid_result(result: &Result<Data>) {
assert!(result.is_ok(), "result should be Ok");
let data = result.as_ref().unwrap();
assert!(!data.is_empty(), "data should not be empty");
}
#[test]
fn test_multiple_users() {
let alice = create_test_user("alice");
let bob = create_test_user("bob");
assert_ne!(alice.id(), bob.id());
}
}
Running Tests
# Run all tests (unit + integration + doc tests)
cargo test
# Run only unit tests
cargo test --lib
# Run only integration tests
cargo test --test '*'
# Run specific integration test file
cargo test --test api_tests
# Run only doc tests
cargo test --doc
# Run with specific feature flags
cargo test --features "std"
cargo test --no-default-features
# Run benchmarks
cargo bench
cargo bench --bench crate_name_parsing
Valid Test Requirements
Tests are considered valid when they:
- ✅ Compile with
cargo test - ✅ Have explicit assertions on outputs
- ✅ Test both valid and invalid inputs
- ✅ Test error paths, not just success
- ✅ Don't test implementation details
- ✅ Are properly isolated (use
current_threadfor async) - ✅ Have clear, descriptive names
- ✅ Include documentation comments for complex tests
Learning Log
2026-01-28: Skill Restructuring
Issue: Original skill.md was 1059 lines with extensive duplication.
Learning: Consolidated feature-gating patterns, removed duplicated test organization examples, streamlined into focused sections.
New Standard: Test skill reduced to ~450 lines focusing on core patterns and anti-patterns.
2026-01-27: False Positive Test Prevention
Issue: Tests creating variables without validation were passing CI but catching no bugs.
Learning: Every test must have at least one explicit assertion validating behavior.
Standard: All tests must validate actual outputs, not just execute code paths.
Examples
See examples/ directory for comprehensive guides:
intro-to-property-based-testing.md- Complete beginner to advanced guide on property-based testing with proptest, including exercises and real-world examples
Related Skills
- Rust Clean Implementation - For implementation patterns
- Rust with Async Code - For async testing patterns
Last Updated: 2026-01-28 Version: 3.1-approved
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?