Rust Testing Excellence
When to Use This Skill
Read this skill when writing or reviewing tests. For implementation patterns, see rust-clean-implementation. For async code, see rust-with-async-code.
🎯 Core Principles (CRITICAL - Always Apply)
1. Real Code Over Mocks 🚨
The Fundamental Rule: Tests must validate actual code behavior, not mock behavior.
✅ VALID Mock Usage (External Only):
- Third-party services (Stripe, payment gateways)
- System resources (hardware devices, OS calls)
- Error injection (disk full, network partition)
❌ INVALID Mock Usage (Our Own Code):
- HTTP clients → Use real test servers
- Databases → Use testcontainers/Docker
- File I/O → Use
tempfilewith real filesystem - Internal services → Test the real thing
The Three Questions Before Every Mock:
- "Is this really external (third-party/OS)?" - If it's yours, NO MOCK
- "Am I testing real logic or mock setup?" - If just mock config, INVALID
- "Are integration points tested separately?" - Need real tests too
📖 See examples: integration-theater-vs-real-testing.md
2. The Three Test Validations ✅
Every test MUST validate:
- ✅ Valid input produces expected output
- ✅ Invalid input is properly rejected with clear errors
- ✅ Edge cases are handled (empty, max, boundary)
// ✅ Example - All three validations
#[test]
fn test_valid_registration() {
let user = register("alice", "alice@example.com").unwrap();
assert_eq!(user.username, "alice");
}
#[test]
fn test_invalid_email_rejected() {
assert!(register("bob", "not-an-email").is_err());
}
#[test]
fn test_empty_username_rejected() {
assert!(register("", "test@example.com").is_err());
}
📖 See complete guide: three-test-validations.md
3. Test Organization (MUST READ)
CRITICAL: ALL tests go in tests/ directory. NO #[cfg(test)] modules in source files.
crate_root/
├── src/ # NO tests here
├── tests/ # ALL Rust tests here
│ ├── units/ # Unit tests: {crate}_{module}_tests.rs
│ └── integration/ # Rust integration tests: {crate}_{workflow}.rs
├── integration/ # Cross-language / external-runtime harnesses for THIS crate
└── benches/ # Benchmarks
📖 MUST READ: test-organization.md - Complete structure and examples
Crate-owned integration harnesses 🚨
RULE: Integration tests and harnesses for a specific crate live inside that crate, never
in a top-level project /integrations directory. This keeps everything for a crate discoverable
in one place.
- Rust integration tests →
{crate}/tests/integration/. - Cross-language / external-runtime harnesses (e.g. a
node:testsuite driving a crate's WASM/JS runtime, a Deno/browser runner, fixtures, mock host/DOM, and any helper build scripts) →{crate}/integration/(a sibling ofsrc/andtests/). - If a harness spans two crates, it lives with the crate that owns the surface under test
(e.g.
foundation_wasm's ABI runtime tests live infoundation_wasm/integration/; the DOM-layer tests live infoundation_wasm_ui/integration/and reach the sibling crate via relative paths). - A nested helper crate used only by the harness (e.g. a wasm fixture module) is a standalone
crate (its own empty
[workspace]) under{crate}/integration/, and added to the workspaceexcludeso it doesn't pollute the main build / feature unification. - Keep these out of the published package (e.g.
include = ["/src", ...]inCargo.toml).
Why: a crate's tests — at every level, in every language — should travel with the crate, not be scattered in a shared top-level folder that hides which crate they belong to.
🔧 Mandatory Workflow Principles
🚨 ONE Test at a Time (CRITICAL)
⚠️ MANDATORY: Write ONE test, make it pass, THEN move to next test.
TDD Cycle:
🔴 Write ONE test → Verify FAILS → Implement → Verify PASSES → Refactor
↓ ONLY THEN
🔴 Write NEXT test → ...
Never:
- ❌ Write multiple tests at once
- ❌ Generate test file with all tests
- ❌ Skip ahead before current test passes
📖 Complete TDD workflow: Test-Driven Development
Always Update tests/mod.rs
When writing test files, add them to tests/mod.rs so Rust includes them:
// tests/mod.rs or tests/units/mod.rs
mod myapp_parser_tests;
mod myapp_validation_tests;
Run Correct Package
# Identify the right package
cargo test --package crate_name
# Verify tests are actually running
cargo test -- --list
No False Claims
- ❌ Empty test bodies
- ❌
assert!(true)to fake passing - ❌ Variables calculated but never asserted
// ❌ BAD - Cheating
#[test]
fn test_logic() { }
#[test]
fn test_logic() { assert!(true) }
// ✅ GOOD - Real test
#[test]
fn test_logic() {
let result = compute(input);
assert_eq!(result, expected);
}
📚 Testing Patterns (Read When Needed)
When Testing HTTP
Decision: Project types → Stdlib → External deps
- Does project have HTTP types? → Create
foundation_testingcrate - Can stdlib do it? → Use
std::net::TcpListener - Need external dep? → Use minimal dep like
tiny_http
📖 Read when implementing: http-testing-with-project-types.md, tcp-testing-stdlib.md
When Testing Databases
Decision tree:
- Can run in Docker? → Use docker-compose or testcontainers ✅ BEST
- Test instance available? → Ask dev team for credentials
- Can use SQLite? → Use
:memory:for SQL - Must mock? → Last resort only
📖 Read when implementing: docker-for-testing.md, testcontainers-examples.md
Quick example:
[dev-dependencies]
testcontainers = "0.15"
let docker = clients::Cli::default();
let postgres = docker.run(images::postgres::Postgres::default());
// Test with real PostgreSQL
When Testing with Features
Use module-level gates, not individual #[cfg] attributes:
#[cfg(test)]
mod tests {
#[cfg(not(feature = "std"))]
mod no_std_tests { /* ... */ }
#[cfg(feature = "std")]
mod std_tests { /* ... */ }
}
📖 Read when implementing: feature-gated-tests.md
When Testing Properties (Not Specific Values)
Use property-based testing for:
- Roundtrip properties (serialize/deserialize)
- Invariants (sorted output stays sorted)
- Mathematical properties (commutative, associative)
use proptest::prelude::*;
proptest! {
#[test]
fn test_roundtrip(data in any::<MyData>()) {
let json = to_json(&data).unwrap();
let parsed = from_json(&json).unwrap();
assert_eq!(data, parsed);
}
}
📖 Read when implementing: property-based-testing-basics.md (quick), intro-to-property-based-testing.md (comprehensive)
⚠️ Common Mistakes (Read When Issues Occur)
Pitfall 1: Testing Implementation Details
❌ Testing internal state → ✅ Test observable behavior
Pitfall 2: No Error Path Testing
❌ Only success cases → ✅ Test both success and failure
Pitfall 3: Missing Initialization
❌ Tests pass for wrong reasons → ✅ Proper setup before each test
Pitfall 4: Muted Variables
❌ Calculate but don't assert → ✅ Explicit assertions on all results
📖 Read when debugging: common-pitfalls.md
🚀 Running Tests
# Basic
cargo test # All tests
cargo test test_name # Specific test
cargo test -p crate_name # Specific package
# Features
cargo test --all-features # All features
cargo test --no-default-features # no_std
cargo test --features "feature" # Specific feature
# Ignored tests (network/slow)
cargo test -- --ignored # Only ignored
cargo test -- --include-ignored # All tests
# Debug
cargo test -- --nocapture # Show output
cargo test -- --test-threads=1 # Sequential
📖 Complete reference: running-tests.md
✅ Test Requirements Checklist
Every test must have:
- Clear name describing what is tested
- Explicit assertions (no muted variables)
- Both success AND failure paths tested
- Edge cases covered (empty, max, boundary)
- Real dependencies (no mocks for our code)
- Proper setup/initialization
- Clear error messages in assertions
Forbidden:
- ❌ Tests in source files (
src/) - ❌ Empty test bodies or
assert!(true) - ❌ Mocking our own HTTP/database/services
- ❌ Tests without error path validation
📖 When to Read Example Files
Always read first:
- ⭐
test-organization.md- Test file structure (MUST READ)
Read when you need to:
- Writing tests:
three-test-validations.md- Valid/invalid/edge pattern - Mocking decisions:
integration-theater-vs-real-testing.md- Real vs mock examples - HTTP testing:
http-testing-with-project-types.md- Use project's types - TCP testing:
tcp-testing-stdlib.md- Pure stdlib approach - Database testing:
testcontainers-examples.md- PostgreSQL/Redis/MongoDB - Docker setup:
docker-for-testing.md- docker-compose + CI - Dependencies:
test-dependency-decisions.md- Decision trees - Feature flags:
feature-gated-tests.md- Module-level gates - Property testing:
property-based-testing-basics.md- Quick patterns - Debugging:
common-pitfalls.md- Common mistakes - Running tests:
running-tests.md- Cargo commands
🔗 Related Skills
- rust-clean-implementation - Implementation patterns and error handling
- rust-with-async-code - Async code patterns and runtime management