rust-testing-code-review

$npx mdskill add existential-birds/beagle/rust-testing-code-review

Reviews Rust test code for patterns, async, mocking, and edition changes.

  • Ensures test code follows idiomatic patterns and best practices.
  • Depends on Rust compiler, tokio, and property-based testing libraries.
  • Analyzes test organization, assertions, isolation, and coverage patterns.
  • Outputs structured review findings with specific recommendations.

SKILL.md

.github/skills/rust-testing-code-reviewView on GitHub ↗
---
name: rust-testing-code-review
description: 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.
---

# Rust Testing Code Review

## Review Workflow

1. **Check Rust edition** — Note edition in `Cargo.toml` (2021 vs 2024). Edition 2024 changes temporary scoping in `if let` and tail expressions, and makes `#[expect]` the preferred lint suppression
2. **Check test organization** — Unit tests in `#[cfg(test)]` modules, integration tests in `tests/` directory
3. **Check async test setup** — `#[tokio::test]` for async tests, proper runtime configuration. Check for `async-trait` on mocks that could use native `async fn` in traits
4. **Check assertions** — Meaningful messages, correct assertion type. Review `if let` assertions for edition 2024 temporary scope changes
5. **Check test isolation** — No shared mutable state between tests, proper setup/teardown. Prefer `LazyLock` over `lazy_static!`/`once_cell` for shared fixtures
6. **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).

1. **Edition recorded** — Open the target crate’s `Cargo.toml` (or workspace `[workspace.package]` / inherited edition) and note the `edition` value. **Pass:** you can quote `edition = "…"` (or document “inherited from workspace”) before citing Rust 2024–specific behavior (`if let` / tail temporary drops, `#[expect]` vs `#[allow]` migration, native `async fn` in traits as default). If edition is not `2024`, do **not** report those items as edition-2024 regressions; at most **Informational** if still useful.
2. **`dyn` vs static async mocks** — Before suggesting native `async fn` in traits instead of `async-trait`, check whether the mock is used as `dyn Trait`. **Pass:** if `dyn` is required, you either skip that suggestion or align with **Valid Patterns** (`async-trait` still needed).
3. **Verification protocol** — **Pass:** steps from the [review-verification-protocol](../review-verification-protocol/SKILL.md) skill are done before any finding is listed (see **Before Submitting Findings**).

## Output Format

Report findings as:

```text
[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](references/unit-tests.md) |
| Integration tests, async testing, fixtures, test databases, native `async fn` mocks, `if let` temporary scope | [references/integration-tests.md](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](references/advanced-testing.md) |
| Loom interleaving tests, Miri UB checks, shuttle, ThreadSanitizer, CI matrix for concurrent code | [references/concurrency-testing.md](references/concurrency-testing.md) |

## Review Checklist

### Test Structure
- [ ] Unit tests in `#[cfg(test)] mod tests` within 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_on` inside async tests (use `.await` directly)
- [ ] Test timeouts set for tests that could hang
- [ ] Mock traits use native `async fn` instead of `async-trait` crate (stable since Rust 1.75)

### Assertions
- [ ] `assert_eq!` / `assert_ne!` used for value comparisons (better error messages than `assert!`)
- [ ] 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 let` assertions 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::LazyLock` used for shared test fixtures instead of `lazy_static!` or `once_cell` (stable since Rust 1.80)

### Error Path Testing
- [ ] `Result::Err` variants tested, not just happy paths
- [ ] Specific error variants checked (not just "is error")
- [ ] `#[should_panic]` used sparingly — prefer `Result`-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 `mod` blocks for organization
- [ ] Test names follow pattern: `<function>_should_<behavior>_when_<condition>`

### Snapshot Testing
- [ ] `cargo insta` used 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
- [ ] `rstest` used 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 `/// # Examples` with runnable code
- [ ] Doc tests serve as both documentation and correctness checks
- [ ] Hidden setup lines prefixed with `#` to keep examples clean
- [ ] `cargo test --doc` passes (nextest doesn't run doc tests)

### Concurrency Testing
> Detailed guidance: [references/concurrency-testing.md](references/concurrency-testing.md)
- [ ] Hand-rolled atomics / `unsafe impl Send|Sync` / state machines have a `#[cfg(loom)]` test using `loom::sync` and `loom::thread` shims (not `std::sync`)
- [ ] Crates with `unsafe` touching atomics, pointers, or `UnsafeCell` run `cargo +nightly miri test --all-features` in CI on every PR (not release-only, not module-level `cfg_attr(miri, ignore)`)
- [ ] Lock-free data structures (`AtomicPtr` stacks/queues/lists) have loom + Miri (Stacked Borrows and Tree Borrows) + `proptest`-driven operation sequences
- [ ] Nondeterministic inputs (`Instant::now`, `rand`, env, `HashMap` iteration) are kept out of `loom::model` bodies
- [ ] Loom jobs run in `--release` with a `LOOM_MAX_PREEMPTIONS` bound; loom tests live in a separate `tests/` file so `--cfg loom` does not poison normal `cargo test`
- [ ] `nextest run -j1` is not cited as evidence of race-condition coverage; FFI-heavy `unsafe extern "C"` paths have a ThreadSanitizer job

### Test Augmentation (Fakes, Mocks, Stubs, Spies)
> Detailed guidance: [references/advanced-testing.md](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](references/advanced-testing.md)
- [ ] Benchmarks use `criterion::black_box(...)` to prevent constant-folding; pointer-flavored inputs use `black_box(input.as_ptr())` not `black_box(&input)`
- [ ] Benchmarks isolate I/O — setup (file open, allocation) in `iter_batched` setup 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) — use `criterion` exclusively
- [ ] 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](references/advanced-testing.md)
- [ ] Table-driven inputs use `rstest` with `#[case::name(...)]` (descriptive case names in test output)
- [ ] Multi-axis input matrices use `rstest` with `#[values(...)]` on multiple parameters (Cartesian product), not hand-expanded test functions
- [ ] Per-input-named tests for large corpora use `paste!` macro or `build.rs` codegen (one `#[test] fn` per file/case)
- [ ] Generated tests have stable names that survive CI log diffs

### Proc-Macro UI Tests (trybuild)
> Detailed guidance: [references/advanced-testing.md](references/advanced-testing.md)
- [ ] Proc-macros that emit compile errors have `trybuild::TestCases` covering each failure path with a `.stderr` reference
- [ ] `.stderr` outputs were regenerated after the latest rustc bump (otherwise spurious CI failures); `TRYBUILD=overwrite cargo test` is documented in CONTRIBUTING
- [ ] CI pins a specific stable rustc for trybuild jobs (the `.stderr` format shifts between stable releases)
- [ ] trybuild tests skipped on nightly (nightly diagnostics differ from stable)

### Clippy Lint Group Strategy
> Detailed guidance: [references/advanced-testing.md](references/advanced-testing.md)
- [ ] `clippy::correctness` is `deny` (always — these are bugs)
- [ ] `clippy::suspicious` is `warn` or `deny` for libraries
- [ ] `clippy::perf` is `warn` (real wins on hot paths)
- [ ] `clippy::pedantic` enabled for libraries; suppressions use `#[expect(clippy::lint_name, reason = "...")]` with justification, not bare `#[allow]`
- [ ] `clippy::nursery` is NOT enabled in CI (experimental; changes between rustc releases)
- [ ] `clippy::restriction` lints 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]` without `expected` message (catches any panic, including wrong ones)
- `unwrap()` in test setup that hides the real failure location
- Tests that depend on execution order
- `if let` with inline temporary in assertion that breaks under edition 2024 temporary scoping
- `async-trait` on mock traits when native `async fn` in traits is available and project targets edition 2024

### Minor
- Missing assertion messages on complex comparisons
- `assert!(x == y)` instead of `assert_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 suppression
- `lazy_static!` or `once_cell` used for test fixtures when `LazyLock` is available

### Informational
- Suggestions to add property-based tests via `proptest` or `quickcheck`
- 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 behavior
- **`use super::*` in test modules** — Standard pattern for accessing parent items
- **`#[allow(dead_code)]` on test helpers** — Helper functions may not be used in every test
- **`clone()` 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()` on `Result`-returning test functions** — Propagating with `?` is also fine but not required
- **`async-trait` on mock traits requiring `dyn` dispatch** — Native `async fn` in traits doesn't support `dyn Trait`; `async-trait` is still needed there
- **`#[expect]` with justification on test helpers** — Self-cleaning lint suppression is correct in test code
- **`LazyLock` for expensive shared test fixtures** — Thread-safe lazy init is appropriate for test globals

## Before Submitting Findings

Load and follow the [review-verification-protocol](../review-verification-protocol/SKILL.md) skill before reporting any issue.

More from existential-birds/beagle

SkillDescription
adr-decision-extractionUse when you need to mine a conversation, session transcript, or design discussion for architectural decisions before writing ADRs. Identifies problem-solution pairs, trade-off debates, technology choices, and explicit \"[ADR]\" tags. Triggers on \"what decisions did we make\", \"extract decisions from this chat\", \"find the choices in our discussion\", or \"summarize architectural decisions\". Also useful after long planning sessions to capture decisions that were made implicitly. Does NOT write ADR documents \u2014 use adr-writing or write-adr for that.
adr-writingUse when writing or formatting an ADR document using the MADR template, applying Definition of Done (E.C.A.D.R.) criteria, or verifying ADR completeness. Triggers on \"write the ADR\", \"format as MADR\", \"check ADR quality\", \"mark gaps in ADR\". Also triggers when a decision has been extracted and needs to become a document. Does NOT extract decisions from conversations (use adr-decision-extraction) or orchestrate the full extract-confirm-write workflow (use write-adr).
agent-architecture-analysisUse when auditing an agent codebase against the 12-Factor Agents methodology, reviewing LLM-powered system architecture, or assessing agentic app compliance. Triggers on \"analyze agent architecture\", \"12-factor audit\", \"how compliant is this agent\", or \"evaluate this LLM app\". Also applies when comparing frameworks or planning agent improvements. Not for quick checklists \u2014 this performs deep per-factor codebase analysis with file-level evidence.
app-intents-code-reviewReviews App Intents code for intent structure, entities, shortcuts, and parameters. Use when reviewing code with import AppIntents, @AppIntent, AppEntity, AppShortcutsProvider, or @Parameter.
artifact-analysisUse when the user wants a cited, structured read of local documents and project knowledge. Triggers on: \"analyze these docs\", \"scan my project for context\", \"read the docs folder\", \"summarize what's in .beagle/concepts/\", \"extract context from docs/\", \"what's in this folder\", \"go read everything in X and tell me what's there\". Also invoked programmatically by other beagle skills (prfaq-beagle Ignition, brainstorm-beagle reference points, strategy-interview context grounding) via the companion contract. Does NOT trigger on codebase lookups (\"find this function\", \"search the repo\"), web research (use web-research), LLM-as-judge evaluation (use llm-judge), or document editing (use humanize-beagle). Produces a written scan plan, parallel-subagent findings, and a cited synthesis report on disk — never inline prose, never unsourced claims.
axum-code-reviewReviews axum web framework code for routing patterns, extractor usage, middleware, state management, and error handling. Use when reviewing Rust code that uses axum, tower, or hyper for HTTP services. Covers axum 0.7+ patterns including State, Path, Query, Json extractors.
brainstorm-beagleUse when the user has a fuzzy idea and wants to shape it into a concrete project spec before planning or building. Triggers on: \"brainstorm this\", \"I have an idea for...\", \"help me think through this project\", \"what should I build\", \"spec this out\". Also catches vague feature descriptions needing structured questioning to clarify scope. Does NOT write code, plan implementation, review strategy docs, or run strategy interviews \u2014 produces a WHAT/WHY spec through dialogue, not a HOW plan.
bubbletea-code-reviewReviews BubbleTea TUI code for proper Elm architecture, model/update/view patterns, and Lipgloss styling. Use when reviewing terminal UI code using charmbracelet/bubbletea.
cloudkit-code-reviewReviews CloudKit code for container setup, record handling, subscriptions, and sharing patterns. Use when reviewing code with import CloudKit, CKContainer, CKRecord, CKShare, or CKSubscription.
combine-code-reviewReviews Combine framework code for memory leaks, operator misuse, and error handling. Use when reviewing code with import Combine, AnyPublisher, @Published, PassthroughSubject, or CurrentValueSubject.