name: rust-testing description: >- Rust testing conventions and frameworks: built-in #[test], integration, and doctest layout plus the cargo-nextest runner and the proptest, insta, criterion, mockall, and rstest ecosystem. Invoke whenever task involves any interaction with Rust tests — writing, running, configuring, or debugging tests in .rs files and Cargo projects.
Rust Testing
Prefer real implementations and hand-written fakes over heavy mocking — a test wired to a mock asserts how the code calls its collaborators, not what the code does. Tests are documentation that runs: a reader should learn the intended behavior from the test name, the arrange/act/assert shape, and the doctest examples on public items. Test the public contract, not the private call graph.
Route to Reference
- Test layout depth —
${CLAUDE_SKILL_DIR}/references/layout.mdUnit vs integration vs doctest mechanics,tests/crate-per-file model,tests/common/mod.rsshared-helper pattern, binary-cratelib.rssplit, doctest attributes (no_run,should_panic,ignore, hidden#lines),assert_matches!andpretty_assertionsusage - Property and snapshot testing, benchmarks —
${CLAUDE_SKILL_DIR}/references/property-and-snapshot.mdproptest vs quickcheck strategy authoring and shrinking, insta inline/file snapshots andcargo insta review, criterion harness setup andstd::hint::black_box - Mocking and fixtures —
${CLAUDE_SKILL_DIR}/references/mocking-and-fixtures.mdmockall#[automock]vsmock!, predicates and call counts, the trait-seam pattern for in-memory fakes, rstest#[case]/#[fixture]params, serial_test, assert_cmd + predicates for CLI integration - Async tests —
${CLAUDE_SKILL_DIR}/references/async-tests.md#[tokio::test]flavors,tokio::time::pause/advancefor deterministic timers, tokio-test macros; cross-references therustskill async reference for runtime semantics - Pinned crate versions —
${CLAUDE_SKILL_DIR}/references/versions.mddate-stamped dev-dependency versions for the testing ecosystem crates ("as of 2026-06")
Read the relevant reference before writing non-trivial tests in that area.
Test Layout
- Place unit tests in a
#[cfg(test)] mod testsblock in the same file as the code under test; bring the parent into scope withuse super::*.#[cfg(test)]keeps test code (and its helpers) out of the build artifact. - Use unit tests to exercise private items — Rust's privacy rules let a child
testsmodule reach its ancestors, so testing internals is possible. Reserve this for genuinely tricky private logic; prefer driving private code through the public API. - Place integration tests in the top-level
tests/directory, next tosrc/. Cargo compiles each file as its own crate, so they reach only the public API viause my_crate::...— exactly as an external consumer would. - Share integration-test helpers from
tests/common/mod.rs, nottests/common.rs. Themod.rsform is not compiled as a separate test crate, so it produces no spurious empty "running 0 tests" section. Import it withmod common;. - For a binary crate, keep
main.rsa thin shell that calls intolib.rs; integration tests then exercise the library crate, since only library crates expose items to externaluse. - Write doctests as
///examples on public items. They double as compiled documentation and run undercargo test --doc. Hide setup lines with a leading#and use```no_runfor examples that must compile but not run.
Assertions
- Use
assert!for booleans andassert_eq!/assert_ne!for value equality; add a trailing message argument when the failure is not self-explanatory. - Use
assert_matches!to assert a value matches a pattern (including enum variants with bindings) — it is stable instdas of Rust 1.96 and requiresuse std::assert_matches::assert_matches;. - Add
pretty_assertionsas a dev-dependency anduse pretty_assertions::{assert_eq, assert_ne};in test modules when comparing large structs or collections — it renders colored line-by-line diffs instead of one unreadable line.
Runner
- Run unit and integration tests with
cargo nextest run— it builds each test binary, queries it for tests, and runs every test in its own process in parallel, giving clean per-test isolation and output. - Always pair it with
cargo test --doc. Nextest does not run doctests, so a project that only runs nextest silently skips every doctest. Both commands belong in the verification gate. - Scope runs with the same selectors as cargo:
-p <pkg>/--workspace,--lib/--test <name>,-E <filterset>for expression filtering, and--no-captureto see test stdout/stderr. - Drive flaky-test triage with
--retries <N>and stop early with--fail-fast; prefer fixing nondeterminism over leaving retries on permanently.
Mocking Decision Rule
Default to the cheapest test double that still proves the behavior, escalating only when the boundary forces it:
- Real implementation — call the actual code. The default for pure logic and anything with no external dependency.
- In-memory fake — a real working implementation backed by memory (e.g. a
HashMap-backed store behind a repository trait). Preferred for stateful collaborators; the fake is reusable across many tests. - Hand-written double — a small struct implementing the collaborator's trait with canned answers. Use when a fake is overkill and you need one or two stubbed responses.
- mockall — generate a mock with
#[automock](ormock!for foreign/multi-trait types) only at IO or nondeterministic boundaries: network, filesystem, clock, randomness, external services behind a trait seam.
Define collaborators behind a trait so the production type and the test double are interchangeable. Assert on observable
outcomes — return values, resulting state, emitted effects — not on which methods were called in which order. Do not use
mockall Sequence/in_sequence to lock internal call order; ordering assertions make tests break on harmless
refactors. Argument matchers and call counts are acceptable when the call itself is the contract (e.g. "charge the card
exactly once").
Property, Snapshot, and Benchmarks
- Reach for property testing when a function must hold an invariant across a large input space (round-trips,
idempotence, ordering). Use
proptest(generates and shrinks values from declarative strategies) orquickcheck(derives generators fromArbitrary); proptest is the richer default for custom input domains. - Use
instafor snapshot testing of large or structural output (serialized data, rendered text, debug dumps). Review and accept changes withcargo insta review; commit the approved.snapfiles. Snapshots make intent diffable rather than hand-asserting every field. - Put benchmarks in
benches/withcriterion, setharness = falseon the[[bench]]target in Cargo.toml, and wrap benchmarked inputs/outputs instd::hint::black_boxso the optimizer cannot fold the work away. Benchmarks measure; they are not correctness tests.
Async Tests
- Annotate async tests with
#[tokio::test]. Default to the current-thread flavor; use#[tokio::test(flavor = "multi_thread", worker_threads = N)]only when the test genuinely needs real parallelism. - Make time deterministic with
tokio::time::pause()andtokio::time::advance(duration)instead of realsleep. Advancing virtual time fires timers instantly and removes wall-clock flakiness from timeout and interval tests. - Mock async-trait collaborators with mockall (compatible with
async_trait/trait_variant), returning futures via.returning(|| Box::pin(async { ... })). The mocking decision rule applies unchanged — mock only at IO boundaries.
Application
When writing tests:
- Apply these conventions silently; do not narrate each rule. Name tests for the behavior under test, not the method.
- Match the surrounding project: if it already standardizes on
cargo test, rstest fixtures, or a specific assertion crate, follow that and flag any divergence once rather than rewriting wholesale. - Add the verification gate commands you rely on (
cargo nextest run,cargo test --doc) when introducing tests to a project that lacks them.
When reviewing tests:
- Flag mocks at non-IO boundaries, assertions on internal call order, and doctests that would be skipped by a nextest-only gate. Cite the specific test and show the corrected form inline.
- Flag nondeterminism (real sleeps, wall-clock reads, unseeded randomness, shared global state without
serial_test) as a flaky-test risk and propose the deterministic alternative.
Integration
The rust skill governs language conventions (ownership, error handling, module layout, the cargo/clippy/rustfmt
gates); this skill governs test structure and the testing ecosystem. When both apply, defer to rust for how the code
under test is written and to this skill for how it is exercised. For async runtime semantics behind #[tokio::test]
(executor model, Send bounds, cancellation), consult the rust skill's async reference rather than restating it here.
Quality Checks
- Unit tests live in
#[cfg(test)] mod tests; integration tests intests/touch only the public API; shared helpers intests/common/mod.rs. - Every behavior has a clear arrange/act/assert shape and a name that reads as documentation.
- The verification gate runs both
cargo nextest runandcargo test --doc— doctests are never silently skipped. - Mocking escalates real -> in-memory fake -> hand-written double -> mockall, and stops at the lowest rung that proves the behavior; no assertions on internal call order.
- Async and time-dependent tests are deterministic (paused virtual time, seeded randomness, no real sleeps).