Rust Testing And Quality
Use this skill to build fast feedback loops and credible final evidence for
Rust changes. Prefer repository recipes when they encode toolchain versions,
services, features, databases, or CI parity.
Load rust-engineering when the test task also
changes core Rust implementation. Do not require it for test-only or review-only
work. Load ci-release-engineering when
the change is to hosted CI jobs, matrices, permissions, artifacts, or release
gates rather than the Rust commands those jobs run.
Workflow
- Inspect
Cargo.toml, workspace layout, rust-toolchain, bacon.toml,
.config/nextest.toml, CI files, Justfile/Makefile/scripts, README/AGENTS
docs, and existing tests.
- Define expected behavior before editing. Use TDD for behavior changes and
BDD-style Given/When/Then acceptance criteria for externally visible flows.
- Pick the narrowest useful test level, write or update a failing test when
practical, then implement the smallest change that passes it.
- Iterate with package or test filters. Broaden to workspace and CI-like lanes
only after the local loop is stable.
- Report exact commands, scope, pass/fail result, and any skipped validation.
Test Level Selection
- Unit tests: pure functions, domain rules, error mapping, parsers, algorithms,
state transitions, and edge cases.
- Integration tests: public crate APIs, adapters, persistence boundaries,
service wiring, feature combinations, and cross-module behavior.
- End-to-end tests: externally observable workflows where unit or integration
tests cannot prove behavior.
- Property-style tests: invariants over many inputs, parsers/serializers,
ordering, idempotence, and round trips.
- Rustdoc tests: public examples readers will copy. Remember
cargo nextest
does not run doctests.
- Compile-fail tests or doctests: macro contracts, type-state APIs, trait-bound
errors, and misuse that should not compile.
Desktop GUI Tests
For native desktop GUI changes, load
rust-desktop-gui for framework mechanics and
choose evidence deliberately:
- Unit-test framework-independent model, message, update, validation, and error
transitions without rendering or an event loop.
- Test GUI adapters for callback or event mapping, task ownership, cancellation,
and shutdown. Control timers and events deterministically; do not rely on real
sleeps or scheduler timing.
- Use a framework-supported interaction, screenshot, or headless harness only
after verifying its installed version and repository support.
- Run packaged-artifact smoke tests on each supported Windows, macOS, or Linux
target when platform behavior, assets, rendering, focus, scaling, dialogs, or
installers change. Host-only tests do not establish target-platform evidence.
Async Rust And Tokio Tests
- Use
#[tokio::test] when the test must await async application services,
adapters, channels, timers, spawned tasks, or Tokio I/O. Keep pure domain tests
synchronous and independent from Tokio.
- Test async application services at hexagonal boundaries with fake outbound
ports for repositories, clients, queues, clocks, and external services. The
fake should model success, domain-relevant failures, timeout/cancellation, and
ordering only where those are part of the behavior.
- Test real async adapters with integration tests against the actual framework,
database, queue, filesystem, or client contract. Do not use adapter tests as a
substitute for narrow domain tests.
- For BDD-style flows, drive the public API, inbound adapter, or use case. Avoid
Given/When/Then steps that depend on private tasks, Tokio channels, or database
rows unless those mechanisms are the public contract.
- Test cancellation by creating a
CancellationToken, starting the work, sending
cancellation or closing the relevant channel, then awaiting the JoinHandle or
JoinSet result. Assert externally visible cleanup, not just that a branch was
reached.
- Test timeouts, retries, intervals, and backoff with Tokio time controls such as
#[tokio::test(start_paused = true)], tokio::time::pause, and
tokio::time::advance when the repository's Tokio features support them.
Avoid real sleeps and arbitrary retry delays in tests.
- Coordinate tests with channels, barriers, or observable state instead of racing
the scheduler. Bound all spawned work, close senders, abort only with intent,
and join tasks before the test exits.
- Use the runtime flavor intentionally.
current_thread can make single-threaded
scheduling assumptions visible; the multithreaded runtime is better evidence
for Send server/worker code.
- Avoid nested runtimes in tests. If production code needs explicit runtime
construction, keep it behind a sync boundary and test the async core directly.
Command Strategy
Use the repo's documented command first. When direct Cargo commands are
appropriate, start narrow and then broaden:
For a technical-debt audit of Axum + Leptos SSR, follow the target-aware matrix
in the rust-async-web audit reference.
In particular, do not substitute a host --all-features build for separate SSR
and wasm32-unknown-unknown hydration evidence.
cargo fmt
cargo fmt --check
cargo check -p <package> --all-targets
cargo test -p <package> <test_name>
cargo test --doc -p <package>
cargo nextest run -p <package>
cargo clippy -p <package> --all-targets -- -D warnings
Final or CI-like checks often need broader scope:
cargo check --workspace --all-targets
cargo test --workspace
cargo test --doc --workspace
cargo nextest run --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
Adapt --all-features to the repository's feature policy. Some workspaces have
platform-specific or mutually incompatible features; match CI when final
confidence matters.
Bacon Feedback Loops
Use Bacon for continuous local Cargo feedback only
when the repository adopts it or the task asks to establish that workflow.
Prefer a checked-in bacon.toml over undocumented personal commands, and verify
current Bacon syntax before creating or changing the file. Start from
bacon --init when generating a new configuration.
For a minimal explicit configuration:
default_job = "check"
[jobs.check]
command = ["cargo", "check"]
[jobs.test]
command = ["cargo", "test"]
need_stdout = true
- Run
bacon for default_job and bacon test for the named test job.
- Keep
command as an executable-token array. Match package, workspace, target,
and feature flags to the repository instead of copying broad flags blindly.
- Bacon already watches conventional Rust paths. Add
watch entries only for
relevant nonstandard inputs, or set default_watch = false when the job must
watch only explicitly listed paths.
- Keep long-running Axum or Leptos process jobs in the framework workflow; use
rust-async-web for restart and server/client
coordination.
- Treat Bacon as an iteration loop, not final evidence. Run the repository's
direct or CI-equivalent Cargo commands before handoff.
cargo-nextest
- Use nextest for fast, isolated Rust test execution when the repository uses it
or when direct Cargo tests are too slow for iteration.
- Inspect
.config/nextest.toml before changing profiles, retries, timeouts, or
partitions.
- Filter intentionally:
cargo nextest run -E 'package(<package>)'
cargo nextest run -E 'test(<test-name-substring>)'
cargo nextest run --profile <profile>
- Nextest runs each test in a separate process. Treat missing service, database,
fixture, environment variable, or port setup as invocation evidence until the
failure proves a code regression.
- Retries can characterize flakiness; they should not hide it without a root
cause or explicit repository policy.
Clippy And Formatting
cargo fmt and cargo fmt --check cover formatting only.
cargo clippy complements cargo check and tests; it does not replace them.
- Do not run
cargo clippy --fix without checking the working tree and reading
the generated diff.
- Use
#[expect(..., reason = "...")] only when the project's MSRV/toolchain is
Rust 1.81 or newer and the lint is intentionally inapplicable. For an older
MSRV, use a narrowly scoped #[allow(...)] with an explanatory comment.
Verify the suppression against the intended lint and supported configuration.
- Do not enable the entire
clippy::restriction group globally; choose specific
lints that express project policy.
Rustdoc Tests
- Add doctests for public examples that should stay accurate across refactors.
- Keep examples deterministic, small, and free of secrets, services, ambient
.env, real databases, timing assumptions, and large setup.
- Use
no_run for examples that should compile but not execute, compile_fail
for invalid usage, and ignore only when no reliable executable form exists.
- Document
# Errors, # Panics, and # Safety where callers need those
contracts.
Review Checklist
- Tests name the behavior, not the implementation detail.
- Regression tests fail on the old bug and pass for the right reason.
- BDD scenarios cover observable outcomes; unit tests cover domain edge cases
and invariants.
- Tests are deterministic: no shared global state, order dependence, real clock
sleeps, uncontrolled ports, or cross-test database contamination.
- Async tests await or supervise all spawned work, close channels deliberately,
and exercise cancellation/timeout behavior when those contracts changed.
- Feature flags, target-specific code, examples, macros, doctests, and generated
code are validated when touched.
- CI evidence includes formatting, compile, tests, doctests when relevant,
linting, and repository-specific checks such as SQLx offline metadata when
the change touches queries.
Anti-Patterns
- Only testing the happy path after changing error handling, ownership, or API
contracts.
- Treating snapshot churn, broad mocks, sleeps, retries, or fixture rewrites as
proof of correctness.
- Using
#[tokio::test] for pure domain behavior that would be faster and
clearer as a synchronous unit test.
- Relying on wall-clock sleeps, scheduler luck, detached tasks, or unclosed
channels to make async tests pass.
- Running only nextest when doctests or compile-fail examples carry the changed
contract.
- Silencing Clippy or rustc warnings without explaining why the warning is
intentionally acceptable.
Successful Use
The final handoff states what behavior was protected, which command(s) ran, what
scope they covered, and what residual validation risk remains.
1---2name: rust-testing-quality3description: Rust testing and quality-gate guidance. Use when writing, updating, running, filtering, or reporting Rust unit, integration, end-to-end, property-style, compile-fail, or Rustdoc tests; when applying TDD or BDD to Rust changes; or when using cargo fmt, cargo check, cargo test, cargo test --doc, cargo clippy, cargo-nextest, Bacon/bacon.toml feedback loops, and CI-oriented Rust validation. Do not use for checked-in hosted CI/release-provider configuration except the Rust commands and test lanes it invokes; use ci-release-engineering.4---56# Rust Testing And Quality78Use this skill to build fast feedback loops and credible final evidence for9Rust changes. Prefer repository recipes when they encode toolchain versions,10services, features, databases, or CI parity.1112Load [`rust-engineering`](../rust-engineering/SKILL.md) when the test task also13changes core Rust implementation. Do not require it for test-only or review-only14work. Load [`ci-release-engineering`](../ci-release-engineering/SKILL.md) when15the change is to hosted CI jobs, matrices, permissions, artifacts, or release16gates rather than the Rust commands those jobs run.1718## Workflow19201. Inspect `Cargo.toml`, workspace layout, `rust-toolchain`, `bacon.toml`,21 `.config/nextest.toml`, CI files, Justfile/Makefile/scripts, README/AGENTS22 docs, and existing tests.232. Define expected behavior before editing. Use TDD for behavior changes and24 BDD-style Given/When/Then acceptance criteria for externally visible flows.253. Pick the narrowest useful test level, write or update a failing test when26 practical, then implement the smallest change that passes it.274. Iterate with package or test filters. Broaden to workspace and CI-like lanes28 only after the local loop is stable.295. Report exact commands, scope, pass/fail result, and any skipped validation.3031## Test Level Selection3233- Unit tests: pure functions, domain rules, error mapping, parsers, algorithms,34 state transitions, and edge cases.35- Integration tests: public crate APIs, adapters, persistence boundaries,36 service wiring, feature combinations, and cross-module behavior.37- End-to-end tests: externally observable workflows where unit or integration38 tests cannot prove behavior.39- Property-style tests: invariants over many inputs, parsers/serializers,40 ordering, idempotence, and round trips.41- Rustdoc tests: public examples readers will copy. Remember `cargo nextest`42 does not run doctests.43- Compile-fail tests or doctests: macro contracts, type-state APIs, trait-bound44 errors, and misuse that should not compile.4546## Desktop GUI Tests4748For native desktop GUI changes, load49[`rust-desktop-gui`](../rust-desktop-gui/SKILL.md) for framework mechanics and50choose evidence deliberately:5152- Unit-test framework-independent model, message, update, validation, and error53 transitions without rendering or an event loop.54- Test GUI adapters for callback or event mapping, task ownership, cancellation,55 and shutdown. Control timers and events deterministically; do not rely on real56 sleeps or scheduler timing.57- Use a framework-supported interaction, screenshot, or headless harness only58 after verifying its installed version and repository support.59- Run packaged-artifact smoke tests on each supported Windows, macOS, or Linux60 target when platform behavior, assets, rendering, focus, scaling, dialogs, or61 installers change. Host-only tests do not establish target-platform evidence.6263## Async Rust And Tokio Tests6465- Use `#[tokio::test]` when the test must await async application services,66 adapters, channels, timers, spawned tasks, or Tokio I/O. Keep pure domain tests67 synchronous and independent from Tokio.68- Test async application services at hexagonal boundaries with fake outbound69 ports for repositories, clients, queues, clocks, and external services. The70 fake should model success, domain-relevant failures, timeout/cancellation, and71 ordering only where those are part of the behavior.72- Test real async adapters with integration tests against the actual framework,73 database, queue, filesystem, or client contract. Do not use adapter tests as a74 substitute for narrow domain tests.75- For BDD-style flows, drive the public API, inbound adapter, or use case. Avoid76 Given/When/Then steps that depend on private tasks, Tokio channels, or database77 rows unless those mechanisms are the public contract.78- Test cancellation by creating a `CancellationToken`, starting the work, sending79 cancellation or closing the relevant channel, then awaiting the `JoinHandle` or80 `JoinSet` result. Assert externally visible cleanup, not just that a branch was81 reached.82- Test timeouts, retries, intervals, and backoff with Tokio time controls such as83 `#[tokio::test(start_paused = true)]`, `tokio::time::pause`, and84 `tokio::time::advance` when the repository's Tokio features support them.85 Avoid real sleeps and arbitrary retry delays in tests.86- Coordinate tests with channels, barriers, or observable state instead of racing87 the scheduler. Bound all spawned work, close senders, abort only with intent,88 and join tasks before the test exits.89- Use the runtime flavor intentionally. `current_thread` can make single-threaded90 scheduling assumptions visible; the multithreaded runtime is better evidence91 for `Send` server/worker code.92- Avoid nested runtimes in tests. If production code needs explicit runtime93 construction, keep it behind a sync boundary and test the async core directly.9495## Command Strategy9697Use the repo's documented command first. When direct Cargo commands are98appropriate, start narrow and then broaden:99100For a technical-debt audit of Axum + Leptos SSR, follow the target-aware matrix101in the [`rust-async-web` audit reference](../rust-async-web/references/axum-leptos-debt-audit.md).102In particular, do not substitute a host `--all-features` build for separate SSR103and `wasm32-unknown-unknown` hydration evidence.104105```sh106cargo fmt107cargo fmt --check108cargo check -p <package> --all-targets109cargo test -p <package> <test_name>110cargo test --doc -p <package>111cargo nextest run -p <package>112cargo clippy -p <package> --all-targets -- -D warnings113```114115Final or CI-like checks often need broader scope:116117```sh118cargo check --workspace --all-targets119cargo test --workspace120cargo test --doc --workspace121cargo nextest run --workspace122cargo clippy --workspace --all-targets --all-features -- -D warnings123```124125Adapt `--all-features` to the repository's feature policy. Some workspaces have126platform-specific or mutually incompatible features; match CI when final127confidence matters.128129## Bacon Feedback Loops130131Use [Bacon](https://dystroy.org/bacon/) for continuous local Cargo feedback only132when the repository adopts it or the task asks to establish that workflow.133Prefer a checked-in `bacon.toml` over undocumented personal commands, and verify134current Bacon syntax before creating or changing the file. Start from135`bacon --init` when generating a new configuration.136137For a minimal explicit configuration:138139```toml140default_job = "check"141142[jobs.check]143command = ["cargo", "check"]144145[jobs.test]146command = ["cargo", "test"]147need_stdout = true148```149150- Run `bacon` for `default_job` and `bacon test` for the named test job.151- Keep `command` as an executable-token array. Match package, workspace, target,152 and feature flags to the repository instead of copying broad flags blindly.153- Bacon already watches conventional Rust paths. Add `watch` entries only for154 relevant nonstandard inputs, or set `default_watch = false` when the job must155 watch only explicitly listed paths.156- Keep long-running Axum or Leptos process jobs in the framework workflow; use157 [`rust-async-web`](../rust-async-web/SKILL.md) for restart and server/client158 coordination.159- Treat Bacon as an iteration loop, not final evidence. Run the repository's160 direct or CI-equivalent Cargo commands before handoff.161162## cargo-nextest163164- Use nextest for fast, isolated Rust test execution when the repository uses it165 or when direct Cargo tests are too slow for iteration.166- Inspect `.config/nextest.toml` before changing profiles, retries, timeouts, or167 partitions.168- Filter intentionally:169170```sh171cargo nextest run -E 'package(<package>)'172cargo nextest run -E 'test(<test-name-substring>)'173cargo nextest run --profile <profile>174```175176- Nextest runs each test in a separate process. Treat missing service, database,177 fixture, environment variable, or port setup as invocation evidence until the178 failure proves a code regression.179- Retries can characterize flakiness; they should not hide it without a root180 cause or explicit repository policy.181182## Clippy And Formatting183184- `cargo fmt` and `cargo fmt --check` cover formatting only.185- `cargo clippy` complements `cargo check` and tests; it does not replace them.186- Do not run `cargo clippy --fix` without checking the working tree and reading187 the generated diff.188- Use `#[expect(..., reason = "...")]` only when the project's MSRV/toolchain is189 Rust 1.81 or newer and the lint is intentionally inapplicable. For an older190 MSRV, use a narrowly scoped `#[allow(...)]` with an explanatory comment.191 Verify the suppression against the intended lint and supported configuration.192- Do not enable the entire `clippy::restriction` group globally; choose specific193 lints that express project policy.194195## Rustdoc Tests196197- Add doctests for public examples that should stay accurate across refactors.198- Keep examples deterministic, small, and free of secrets, services, ambient199 `.env`, real databases, timing assumptions, and large setup.200- Use `no_run` for examples that should compile but not execute, `compile_fail`201 for invalid usage, and `ignore` only when no reliable executable form exists.202- Document `# Errors`, `# Panics`, and `# Safety` where callers need those203 contracts.204205## Review Checklist206207- Tests name the behavior, not the implementation detail.208- Regression tests fail on the old bug and pass for the right reason.209- BDD scenarios cover observable outcomes; unit tests cover domain edge cases210 and invariants.211- Tests are deterministic: no shared global state, order dependence, real clock212 sleeps, uncontrolled ports, or cross-test database contamination.213- Async tests await or supervise all spawned work, close channels deliberately,214 and exercise cancellation/timeout behavior when those contracts changed.215- Feature flags, target-specific code, examples, macros, doctests, and generated216 code are validated when touched.217- CI evidence includes formatting, compile, tests, doctests when relevant,218 linting, and repository-specific checks such as SQLx offline metadata when219 the change touches queries.220221## Anti-Patterns222223- Only testing the happy path after changing error handling, ownership, or API224 contracts.225- Treating snapshot churn, broad mocks, sleeps, retries, or fixture rewrites as226 proof of correctness.227- Using `#[tokio::test]` for pure domain behavior that would be faster and228 clearer as a synchronous unit test.229- Relying on wall-clock sleeps, scheduler luck, detached tasks, or unclosed230 channels to make async tests pass.231- Running only nextest when doctests or compile-fail examples carry the changed232 contract.233- Silencing Clippy or rustc warnings without explaining why the warning is234 intentionally acceptable.235236## Successful Use237238The final handoff states what behavior was protected, which command(s) ran, what239scope they covered, and what residual validation risk remains.