Expert-level Rust testing — the "What Could Break?" framework, five transformations from superficial to expert tests, flake hunting protocol, intent-based assertions, naming conventions, and a mandatory self-review checklist. Triggers on writing Rust tests, designing test cases, improving test quality, or reviewing test coverage.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Expert-level Rust testing — the "What Could Break?" framework, five transformations from superficial to expert tests, flake hunting protocol, intent-based assertions, naming conventions, and a mandatory self-review checklist. Triggers on writing Rust tests, designing test cases, improving test quality, or reviewing test coverage.
Rust Test Writing Skill
Write tests that catch real bugs. Every test must guard a specific invariant -- not just prove the code "works."
The "What Could Break?" Framework
Before writing any test, answer these four questions:
What invariant does this code maintain? (e.g., "deserialized config always has a default profile")
What edge case would violate it? (e.g., "empty TOML table, missing key, extra unknown key")
What platform difference could surface? (e.g., "path separators, case sensitivity, symlink behavior")
What would a future refactor accidentally break? (e.g., "field added to struct but not to Display impl")
If you can only answer #1, your test is a happy-path test. Answer all four and you have a regression suite.
The 5 Test Transformations
Each transformation shows a superficial test pattern and its expert replacement.
Factory functions for domain objects let each test construct exactly the fixture it needs. No shared mutable state. No JSON parsing at test time.
Transformation 5: HashMap in fixtures -> BTreeMap for determinism
// BEFORE: test passes 99% of the time, flakes in CIletmut map = HashMap::new();
map.insert("b", 2);
map.insert("a", 1);
assert_eq!(format!("{map:?}"), r#"{"a": 1, "b": 2}"#); // order not guaranteed// AFTER: deterministic iteration orderletmut map = BTreeMap::new();
map.insert("b", 2);
map.insert("a", 1);
assert_eq!(format!("{map:?}"), r#"{"a": 1, "b": 2}"#); // always this order
Use BTreeMap whenever output order affects assertions or snapshots.
Test Flake Hunting Protocol
Bolin's single most frequent pattern (97+ references across 30+ commits). When a test is flaky, follow this exact protocol:
Identify the race window -- read the event-emission code, find where timing assumptions break. Locate the exact line where the test assumes an event has arrived or a state has changed without proof.
Replace timing with event-driven sync -- wait for a specific event instead of sleeping or assuming order. Never use sleep as a synchronization primitive.
Make assertions order-independent -- sort collected values, use sets, or match by content not position. Non-deterministic event ordering is not a bug; asserting on it is.
Stress-test the fix -- run with the exact command: cargo nextest run -p <crate> -j 2 --no-fail-fast --stress-count 50 --status-level leak
Document the non-determinism in the commit message -- explain why the timing assumption was wrong and what synchronization replaced it.
turn/started emitted optimistically before state is actually ready
Event ordering across async channels (mpsc, broadcast)
HashMap iteration order in serialized output
Test harness Drop racing with child process shutdown (close stdin first, then wait, then kill)
Intent-Based Assertions
Replace exact command-string matching with intent-based semantic matching. Check that the test observes the right INTENT (operation + target) rather than a specific command format that varies across platforms or refactors.
// BEFORE: brittle -- breaks if command formatting changesassert_eq!(cmd.to_string(), "rm -rf /tmp/workspace/build");
// AFTER: intent-based -- asserts the operation and targetassert_eq!(cmd.operation(), Operation::Remove);
assert!(cmd.target().ends_with("workspace/build"));
assert!(cmd.is_recursive());
When exact strings are unavoidable, assert on the semantically meaningful parts (path suffix, flag presence) rather than the full formatted string.
Test Naming Convention
Pattern: {subject}_{scenario}_{expected_outcome}
A failed test name must be an actionable bug description. When it fails in CI, the name alone tells you what broke. The name should read as a specification: if it fails, you know exactly what invariant was violated.
wiremock: Any test that would hit a real HTTP endpoint. Mount responses with Mock::given().respond_with(). Assert request bodies after the test.
TempDir: Every test that touches disk. Never mutate the process environment. Never hardcode /tmp or C:\.
insta: TUI widgets, CLI output, error messages -- anything where the exact text matters. Render to a buffer, snapshot with assert_snapshot!.
pretty_assertions: Default for all assert_eq! calls. Gives colored diffs on failure. Import at the top of every test file.
nextest stress: After fixing any flaky test. Always run with -j 2 --no-fail-fast --stress-count 50 to confirm the fix holds under concurrency.
Self-Review Checklist
After writing tests, verify every item. Fix every violation before presenting the tests.
[ ] Uses pretty_assertions::assert_eq (not std assert_eq)
[ ] Compares entire objects, not individual fields
[ ] Each test guards a specific invariant (not just "it works")
[ ] Test names follow {subject}_{scenario}_{expected_outcome}
[ ] Test names encode the invariant being guarded
[ ] TempDir for any filesystem tests (no hardcoded paths)
[ ] No process environment mutation (no std::env::set_var)
[ ] Error paths tested (not just happy path)
[ ] At least 3 tests for any non-trivial function
[ ] BTreeMap used where iteration order affects assertions
[ ] Test file is a sibling _tests.rs, not inline mod tests {}
[ ] No timing-dependent assertions (no sleep -> assert)
[ ] Order-independent where event order is non-deterministic
[ ] String assertions use intent matching, not exact format