Consolidate verbose test suites by replacing repetitive unit tests with property-based tests, parameterized tests (rstest), or fuzz tests. Less code to maintain, same or better coverage.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Consolidate verbose test suites by replacing repetitive unit tests with property-based tests, parameterized tests (rstest), or fuzz tests. Less code to maintain, same or better coverage.
Test Consolidator
Analyze test modules for opportunities to replace many similar unit tests with
a single, more powerful testing construct — property-based tests, parameterized
tests, or fuzz tests. The goal is comprehensive coverage with minimal code to
maintain, review, and keep updated.
When to Use
A test module has 5+ unit tests for the same function with different inputs
Tests follow a pattern: call function with input X, assert output Y
You notice copy-paste test bodies differing only in values
Before a PR when test files are growing faster than source files
After refactoring: old tests may cluster around the same behavior
During periodic test hygiene alongside /test-dedup
Philosophy
Tests should scale with behaviors, not with inputs.
If a function has 20 valid inputs worth testing, the answer is NOT 20 test
functions. The answer is one construct that covers all 20 — and ideally the
infinite space between them.
The hierarchy of consolidation (prefer higher):
Property-based test (proptest) — When you can state an invariant that
holds for ALL valid inputs. One proptest! replaces unbounded unit tests.
Maximum coverage, minimum code.
Parameterized test (rstest) — The default choice for any finite set
of (input, expected) pairs. One #[rstest] with #[case] attributes
replaces N identical test bodies. Each case runs as a named sub-test,
giving clear CI output on failure. Always prefer rstest over table-driven
loops — rstest gives you per-case names, per-case failures, and the
ability to individual cases.
#[ignore]
Fuzz test — When exploring adversarial or untrusted input spaces. One
fuzz target can subsume hundreds of hand-crafted "weird input" tests.
Table-driven test — A distant fallback. Only use when rstest is truly
inappropriate (e.g., dynamically generated case lists, cases loaded from a
file, or >50 cases where #[case] attributes become unwieldy). If you're
reaching for let cases = [...]; for case in cases { ... }, stop and
use rstest instead.
Individual unit tests — The last resort. Only when each test truly
exercises unique setup, distinct error paths, or documents a specific bug.
Never consolidate for the sake of shorter code. The goal is less code to
maintain — meaning fewer places to update when the function signature changes,
fewer tests to rename when behavior evolves, and fewer copy-paste errors.
Analysis Process
Step 1: Identify Test Clusters
A test cluster is a group of tests that:
Test the same function or method
Have the same structure (setup → call → assert)
Differ primarily in input values and expected outputs
May also differ in minor setup variations
Scan the module and group tests into clusters. A single test can belong to
multiple clusters if it tests more than one function.
The only situations where table-driven is acceptable:
Dynamically generated case lists (cases built at runtime)
Cases loaded from an external file
50 cases where #[case] attribute lines become genuinely unwieldy
// ANTI-PATTERN — do NOT write this:#[test]fnstatus_text_cases() {
letcases = [
(200, "OK"),
(201, "Created"),
(400, "Bad Request"),
];
for (code, expected) in cases {
assert_eq!(status_text(code), expected, "code={code}");
}
}
// CORRECT — use rstest instead:#[rstest]#[case(200, "OK")]#[case(201, "Created")]#[case(400, "Bad Request")]fnstatus_text_mapping(#[case] code: u16, #[case] expected: &str) {
assert_eq!(status_text(code), expected);
}
Fuzz Test — Best for adversarial input exploration
Use when: Tests are exploring "weird inputs" to find crashes.
// BEFORE: 12 unit tests trying to crash the parser#[test]fnparse_null_bytes() { let_ = parse(b"\x00\x00"); }
#[test]fnparse_huge_input() { let_ = parse(&vec![0xFF; 10_000]); }
#[test]fnparse_truncated() { let_ = parse(b"\x01\x02"); }
// ... 9 more ...// AFTER: 1 fuzz target (explores billions of inputs)
fuzz_target!(|data: &[u8]| {
let_ = parse(data);
});
// KEEP: 1-2 specific regression tests if they document known bugs
Step 5: Check for Blockers
Before recommending consolidation, verify:
Feature gates: proptest is a direct dev-dependency (no feature gate for
tests). Some crates gate Arbitrary impls behind test-support. If moving
tests behind a feature gate, keep at least one ungated unit test as baseline.
Regression tests: Tests with bug references (// Regression: GH-123)
should stay as individual tests even if technically consolidatable.
Error path tests: Tests verifying specific error messages or error
variants may need individual tests if the exact error matters.
Readability anchors: Keep one simple example test per public function
as documentation, even if a property test covers it.
rstest availability: rstest is a project standard (workspace dep rstest = "0.25").
If not yet in a crate's [dev-dependencies], add rstest.workspace = true. This is
expected and should not be treated as a blocker.
Step 6: Produce Consolidation Plan
For each cluster, specify:
Current state: Number of tests, lines of test code
Recommended strategy: Which tool and why
What gets removed: Which specific tests are replaced
What gets kept: Which tests survive and why
New test code: The actual replacement test(s)
Net effect: Lines removed vs added, maintenance burden change
Project-Specific Conventions
Test locations in this codebase
Inline tests: #[cfg(test)] mod tests { ... } at bottom of source file
Sibling test files: crates/*/src/*_tests.rs (e.g., ring_buffer_tests.rs, error_tests.rs)
Property tests: in #[cfg(test)] modules using proptest (direct dev-dep, no feature gate)
Kani proofs: #[cfg(kani)] blocks in gossip-stdx
Simulation tests: crates/gossip-coordination/src/sim/ (CoordinationSim harness + proptest state machine)
Fuzz targets: crates/gossip-contracts/fuzz/ and crates/gossip-stdx/fuzz/
test-support: Enables Arbitrary impls in gossip-contracts and sim infrastructure in gossip-coordination
kani: Enables Kani model checking proofs in gossip-stdx
tiger-harness: Enables tiger harness in scanner-engine
scheduler-sim: Enables scheduler simulation in scanner-scheduler
sim-harness: Enables simulation harness in scanner-engine and scanner-scheduler
bench: Enables benchmark scaffolding in scanner-engine and scanner-scheduler
Dependencies
rstest: Project standard (workspace dep rstest = "0.25"). Add rstest.workspace = true
to crate-level [dev-dependencies] if not already present. No feature gate needed.
proptest: Direct dev-dependency in most crates. Arbitrary impls for shared types
gated behind test-support in gossip-contracts.
cargo-fuzz: External tool, no Cargo.toml change needed
Output Format
## Test Consolidation Report: [module/file]### Cluster Analysis#### Cluster 1: `function_name()` — N tests, M lines
**Tests in cluster:**
| # | Test | Input | Assertion |
|---|------|-------|-----------|
| 1 | `test_foo_basic` | "hello" | returns "HELLO" |
| 2 | `test_foo_empty` | "" | returns "" |
| 3 | `test_foo_unicode` | "café" | returns "CAFÉ" |
| ... | ... | ... | ... |
**Pattern detected:** All tests call `foo(input)` and assert exact output.
Inputs vary, assertion structure is identical.
**Recommended strategy:** Property-based test
**Rationale:** The invariant `foo(x).to_lowercase() == x.to_lowercase()` holds
for all inputs. A single proptest replaces all 8 unit tests.
**Consolidation:**
- REPLACE tests #1-#6 with proptest `prop_foo_case_invariant`- KEEP `test_foo_empty` as anchor (documents empty-input behavior)
- KEEP `test_foo_regression_gh_42` (regression test, bug reference)
**Proposed code:**```rust
proptest! {
#[test]
fn prop_foo_case_invariant(input in "\\PC{0,100}") {
prop_assert_eq!(foo(&input).to_lowercase(), input.to_lowercase());
}
}
Cluster 2: bar() — N tests, M lines
...
Summary
Metric
Before
After
Change
Total tests
34
12
-22 (65%)
Test lines
280
95
-185 (66%)
Behaviors covered
8
8
No change
Input space covered
~34 points
Continuous
Vastly improved
Dependency Changes
Add rstest.workspace = true to [dev-dependencies] (if rstest recommended)
No new dependencies needed (if only proptest/fuzz)
Add rstest for Cluster 3 (6 tests → 1 parameterized)
Add rstest for Cluster 5 (4 tests → 1 parameterized)
Run full test suite to verify no coverage loss
Delete subsumed unit tests
## Decision Heuristics
### When to prefer proptest over rstest
- You can state a property about the output without knowing the exact value
- The input space is continuous or very large
- Roundtrip properties exist (encode/decode, serialize/deserialize)
- Order/sorting/containment invariants exist
### When to prefer rstest over proptest
- Each case has a specific expected output that must be exact
- The set of important cases is finite and known
- The mapping is arbitrary (no mathematical relationship)
- You want each case to appear as a named sub-test in output
### When to prefer rstest over table-driven (almost always)
- **Default to rstest for ALL finite case sets.** rstest gives you:
- Per-case test names in `cargo test` output (e.g., `status_text_mapping::case_3`)
- Independent execution — one failing case doesn't hide others
- `#[ignore]` on individual cases
- Native `cargo test --test-threads` parallelism per case
- Table-driven is only acceptable for dynamically generated cases or >50 entries
### When to prefer fuzz over all else
- The function processes untrusted/external input
- The goal is "never panic" rather than "correct output"
- You've been writing tests that try to "trick" the parser
### When NOT to consolidate
- Each test has genuinely different setup logic (not just different values)
- Tests verify different error paths with different error types
- Tests are regression tests for specific bugs (keep the history)
- The "consolidated" version would be harder to understand than the originals
- There are only 2-3 tests — consolidation overhead isn't worth it
## Judgment Calls
- **Threshold**: Don't consolidate clusters of fewer than 4 tests unless the
consolidation is obviously cleaner (e.g., perfect roundtrip property).
- **Mixed clusters**: If 6 of 8 tests consolidate but 2 are genuinely unique,
consolidate the 6 and keep the 2. Don't force everything into one construct.
- **Error tests**: Tests for error cases often belong in a separate rstest or
table, not mixed into the happy-path property test. Group by success/failure.
- **Naming**: Consolidated tests should have clear names describing the
invariant or case set, not generic names like `test_all_cases`.
## Related Skills
- `/test-dedup` — Remove tests subsumed by higher-level tests (complementary)
- `/test-strategy` — Decide what kind of test to write for new code