Rust testing code review
Skill existential-birds/beagle/plugins/beagle-rust/skills/rust-testing-code-review
Agent Skills marketplace: framework-aware skills for code review, documentation, test-plan generation, AI-writing detection, architectural analysis, and git workflows — for Python, Go, Rust, Elixir, React, Remix, and iOS/Swift. Works with Claude Code, Codex, and any agent that supports Agent Skills.
npx -y skills add existential-birds/beagle --skill rust-testing-code-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Reviews Rust test code for unit test patterns, integration test structure, async testing, mocking approaches, and property-based testing. Covers Rust 2024 edition changes including async fn in traits for mocks, #[expect] lint suppression, LazyLock test fixtures, and temporary scope changes affecting test assertions. Use when reviewing _test.rs files, #[cfg(test)] modules, or test infrastructure in Rust projects. Covers tokio::test, test fixtures, and assertion patterns.
SKILL.md
14.1 KB, as published. Nobody here has run it
Rust Testing Code Review
Review Workflow
- Check Rust edition — Note edition in
Cargo.toml(2021 vs 2024). Edition 2024 changes temporary scoping inif letand tail expressions, and makes#[expect]the preferred lint suppression - Check test organization — Unit tests in
#[cfg(test)]modules, integration tests intests/directory - Check async test setup —
#[tokio::test]for async tests, proper runtime configuration. Check forasync-traiton mocks that could use nativeasync fnin traits - Check assertions — Meaningful messages, correct assertion type. Review
if letassertions for edition 2024 temporary scope changes - Check test isolation — No shared mutable state between tests, proper setup/teardown. Prefer
LazyLockoverlazy_static!/once_cellfor shared fixtures - Check coverage patterns — Error paths tested, edge cases covered
Gates (hard)
Do not advance to Output Format until each pass condition is satisfied (yes/no with a concrete artifact).
- Edition recorded — Open the target crate’s
Cargo.toml(or workspace[workspace.package]/ inherited edition) and note theeditionvalue. Pass: you can quoteedition = "…"(or document “inherited from workspace”) before citing Rust 2024–specific behavior (if let/ tail temporary drops,#[expect]vs#[allow]migration, nativeasync fnin traits as default). If edition is not2024, do not report those items as edition-2024 regressions; at most Informational if still useful. dynvs static async mocks — Before suggesting nativeasync fnin traits instead ofasync-trait, check whether the mock is used asdyn Trait. Pass: ifdynis required, you either skip that suggestion or align with Valid Patterns (async-traitstill needed).- Verification protocol — Pass: steps from the review-verification-protocol skill are done before any finding is listed (see Before Submitting Findings).
Output Format
Report findings as:
[FILE:LINE] ISSUE_TITLE
Severity: Critical | Major | Minor | Informational
Description of the issue and why it matters.
Quick Reference
| Issue Type | Reference |
|---|---|
Unit tests, assertions, naming, snapshots, rstest, doc tests, #[expect], LazyLock fixtures, tail expression scope | references/unit-tests.md |
Integration tests, async testing, fixtures, test databases, native async fn mocks, if let temporary scope | references/integration-tests.md |
Fuzzing, proptest, Miri, Loom basics, mocking strategies, stub/fake/mock/spy taxonomy, rstest matrix, paste!, build.rs test gen, criterion baselines + black_box discipline, trybuild UI tests, clippy lint groups | references/advanced-testing.md |
| Loom interleaving tests, Miri UB checks, shuttle, ThreadSanitizer, CI matrix for concurrent code | references/concurrency-testing.md |
Review Checklist
Test Structure
- Unit tests in
#[cfg(test)] mod testswithin source files - Integration tests in
tests/directory (one file per module or feature) -
use super::*in test modules to access parent module items - Test function names describe the scenario:
test_<function>_<scenario>_<expected> - Tests are independent — no reliance on execution order
Async Tests
-
#[tokio::test]used for async test functions -
#[tokio::test(flavor = "multi_thread")]when testing multi-threaded behavior - No
block_oninside async tests (use.awaitdirectly) - Test timeouts set for tests that could hang
- Mock traits use native
async fninstead ofasync-traitcrate (stable since Rust 1.75)
Assertions
-
assert_eq!/assert_ne!used for value comparisons (better error messages thanassert!) - Custom messages on assertions that aren't self-documenting
-
matches!macro used for enum variant checking - Error types checked with
matches!or pattern matching, not string comparison - One assertion per test where practical (easier to diagnose failures)
-
if letassertions reviewed for edition 2024 temporary scope — temporaries in conditions drop earlier, may invalidate borrows - Tail expression returns reviewed for edition 2024 — temporaries in tail expressions drop before local variables
Mocking and Test Doubles
- Traits used as seams for dependency injection (not concrete types)
- Mock implementations kept minimal — only what the test needs
- No mocking of types you don't own (wrap external dependencies behind your own trait)
- Test fixtures as helper functions, not global state
-
std::sync::LazyLockused for shared test fixtures instead oflazy_static!oronce_cell(stable since Rust 1.80)
Error Path Testing
-
Result::Errvariants tested, not just happy paths - Specific error variants checked (not just "is error")
-
#[should_panic]used sparingly — preferResult-returning tests
Lint Suppression in Tests
-
#[expect(lint)]used instead of#[allow(lint)]for test-specific suppressions (stable since Rust 1.81) - Justification comment on every
#[expect]or#[allow]in test code - Stale
#[allow]attributes migrated to#[expect]for self-cleaning behavior
Test Naming
- Test names read like sentences describing behavior (not
test_happy_path) - Related tests grouped in nested
modblocks for organization - Test names follow pattern:
<function>_should_<behavior>_when_<condition>
Snapshot Testing
-
cargo instaused for complex structural output (JSON, YAML, HTML, CLI output) - Snapshots are small and focused (not huge objects)
- Redactions used for unstable fields (timestamps, UUIDs)
- Snapshots committed to git in
snapshots/directory - Simple values use
assert_eq!, not snapshots
Parametrized Testing
-
rstestused to avoid duplicated test functions for similar inputs -
#[rstest]with#[case::name]attributes for descriptive parametrized tests -
#[fixture]used for shared test setup when multiple tests need same construction - Parametrized tests still have descriptive case names (not just
#[case(1)]) - Combined with async:
#[rstest] #[tokio::test]for async parametrized tests
Doc Tests
- Public API functions have
/// # Exampleswith runnable code - Doc tests serve as both documentation and correctness checks
- Hidden setup lines prefixed with
#to keep examples clean -
cargo test --docpasses (nextest doesn't run doc tests)
Concurrency Testing
Detailed guidance: references/concurrency-testing.md
- Hand-rolled atomics /
unsafe impl Send|Sync/ state machines have a#[cfg(loom)]test usingloom::syncandloom::threadshims (notstd::sync) - Crates with
unsafetouching atomics, pointers, orUnsafeCellruncargo +nightly miri test --all-featuresin CI on every PR (not release-only, not module-levelcfg_attr(miri, ignore)) - Lock-free data structures (
AtomicPtrstacks/queues/lists) have loom + Miri (Stacked Borrows and Tree Borrows) +proptest-driven operation sequences - Nondeterministic inputs (
Instant::now,rand, env,HashMapiteration) are kept out ofloom::modelbodies - Loom jobs run in
--releasewith aLOOM_MAX_PREEMPTIONSbound; loom tests live in a separatetests/file so--cfg loomdoes not poison normalcargo test -
nextest run -j1is not cited as evidence of race-condition coverage; FFI-heavyunsafe extern "C"paths have a ThreadSanitizer job
Test Augmentation (Fakes, Mocks, Stubs, Spies)
Detailed guidance: references/advanced-testing.md
- Test doubles are typed by purpose: stub (canned data), fake (working but simplified impl), mock (pre-programmed expectations + verification), spy (records calls for after-the-fact inspection) — vocabulary is used precisely
-
mockall-style mocks are not used where a fake would be simpler (in-memory DB beats expectation-heavy mocks for repository-style traits) - Fakes are tested against the same contract as the production impl (trait conformance test); no drift
- Trait-as-seam pattern used to inject test doubles — production code depends on the trait, not the concrete type
Performance Tests (Criterion)
Detailed guidance: references/advanced-testing.md
- Benchmarks use
criterion::black_box(...)to prevent constant-folding; pointer-flavored inputs useblack_box(input.as_ptr())notblack_box(&input) - Benchmarks isolate I/O — setup (file open, allocation) in
iter_batchedsetup closure, NOT inside the measured closure - CI persists a baseline via
cargo bench -- --save-baseline main; feature branches compare with--baseline main; regression threshold defined and enforced - Criterion benchmarks build with
--profile bench(release optimization + debug symbols for flamegraph correlation) - No
#[bench](unstable, deprecated) — usecriterionexclusively - Single-shot timings are not cited as evidence; reports show mean + variance from criterion's statistical engine
Test Generation
Detailed guidance: references/advanced-testing.md
- Table-driven inputs use
rstestwith#[case::name(...)](descriptive case names in test output) - Multi-axis input matrices use
rstestwith#[values(...)]on multiple parameters (Cartesian product), not hand-expanded test functions - Per-input-named tests for large corpora use
paste!macro orbuild.rscodegen (one#[test] fnper file/case) - Generated tests have stable names that survive CI log diffs
Proc-Macro UI Tests (trybuild)
Detailed guidance: references/advanced-testing.md
- Proc-macros that emit compile errors have
trybuild::TestCasescovering each failure path with a.stderrreference -
.stderroutputs were regenerated after the latest rustc bump (otherwise spurious CI failures);TRYBUILD=overwrite cargo testis documented in CONTRIBUTING - CI pins a specific stable rustc for trybuild jobs (the
.stderrformat shifts between stable releases) - trybuild tests skipped on nightly (nightly diagnostics differ from stable)
Clippy Lint Group Strategy
Detailed guidance: references/advanced-testing.md
-
clippy::correctnessisdeny(always — these are bugs) -
clippy::suspiciousiswarnordenyfor libraries -
clippy::perfiswarn(real wins on hot paths) -
clippy::pedanticenabled for libraries; suppressions use#[expect(clippy::lint_name, reason = "...")]with justification, not bare#[allow] -
clippy::nurseryis NOT enabled in CI (experimental; changes between rustc releases) -
clippy::restrictionlints are opt-in individually; the group is never enabled wholesale
Severity Calibration
Critical
- Tests that pass but don't actually verify behavior (assertions on wrong values)
- Shared mutable state between tests causing flaky results
- Missing error path tests for security-critical code
Major
#[should_panic]withoutexpectedmessage (catches any panic, including wrong ones)unwrap()in test setup that hides the real failure location- Tests that depend on execution order
if letwith inline temporary in assertion that breaks under edition 2024 temporary scopingasync-traiton mock traits when nativeasync fnin traits is available and project targets edition 2024
Minor
- Missing assertion messages on complex comparisons
assert!(x == y)instead ofassert_eq!(x, y)(worse error messages)- Test names that don't describe the scenario
- Redundant setup code that could be extracted to a helper
#[allow]used where#[expect]would provide self-cleaning suppressionlazy_static!oronce_cellused for test fixtures whenLazyLockis available
Informational
- Suggestions to add property-based tests via
proptestorquickcheck - Suggestions to add snapshot testing for complex output
- Coverage improvement opportunities
Valid Patterns (Do NOT Flag)
unwrap()/expect()in tests — Panicking on unexpected errors is the correct test behavioruse super::*in test modules — Standard pattern for accessing parent items#[allow(dead_code)]on test helpers — Helper functions may not be used in every testclone()in tests — Clarity over performance- Large test functions — Integration tests can be long; extracting helpers isn't always clearer
assert!for boolean checks — Fine when the expression is clearly boolean (.is_some(),.is_empty())- Multiple assertions testing one logical behavior — Sometimes one behavior needs multiple checks
unwrap()onResult-returning test functions — Propagating with?is also fine but not requiredasync-traiton mock traits requiringdyndispatch — Nativeasync fnin traits doesn't supportdyn Trait;async-traitis still needed there#[expect]with justification on test helpers — Self-cleaning lint suppression is correct in test codeLazyLockfor expensive shared test fixtures — Thread-safe lazy init is appropriate for test globals
Before Submitting Findings
Load and follow the review-verification-protocol skill before reporting any issue.