
Test Consolidate
- 1 installs
- 2 repo stars
- Updated March 2, 2026
- ahrav/scratch-scanner-rs
test-consolidate is a Claude Code skill that replaces repetitive unit tests with property-based, parameterized (rstest), or fuzz tests to reduce test-code maintenance.
About
test-consolidate analyzes test modules for clusters of similar unit tests and replaces them with a more powerful construct: property-based tests (proptest), parameterized tests (rstest), or fuzz tests. It identifies clusters testing the same function with different inputs, classifies the best strategy per cluster, and keeps genuinely unique tests as-is. The goal is less test code to maintain with the same or better coverage. It is Rust-focused.
- Replaces repetitive unit tests with property-based, parameterized, or fuzz tests
- Identifies test clusters and picks a consolidation strategy per cluster
- Targets less code to maintain with equal or better coverage (Rust)
Test Consolidate by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,750 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 7, 2026 (Skillselion catalog sync)
test-consolidate capabilities & compatibility
- Capabilities
- test consolidation · property based testing · parameterized testing · fuzz testing
- Use cases
- testing · refactoring
- Pricing
- Free
What test-consolidate says it does
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.
Tests should scale with behaviors, not with inputs.
Always prefer rstest over table-driven tests
npx skills add https://github.com/ahrav/scratch-scanner-rs --skill test-consolidateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 2 |
| Last updated | March 2, 2026 |
| Repository | ahrav/scratch-scanner-rs ↗ |
What it does
Replace repetitive Rust unit tests with property, parameterized, or fuzz tests for less code and equal or better coverage.
Who is it for?
Rust test modules with clusters of similar unit tests that share a call-then-assert structure.
Skip if: Tests with genuinely unique setup or distinct error paths, which the docs say to keep as individual tests.
When should I use this skill?
A test module has 5+ similar unit tests for the same function, or test files are growing faster than source files before a PR.
What you get
Consolidated tests using proptest, rstest, or fuzz targets that cover the same behaviors with less code.
- consolidated property/parameterized/fuzz tests replacing unit-test clusters
By the numbers
- 4-level consolidation hierarchy (proptest, rstest, fuzz, individual)
- triggers on 5+ similar unit tests per function
Files
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):
1. 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.
2. Parameterized test (rstest) — When you have a finite, important set of (input, expected) pairs and no general invariant. One #[rstest] with a #[case] list replaces N identical test bodies. Always prefer rstest over table-driven tests — rstest gives you named sub-cases in test output, better failure diagnostics, and no manual loop boilerplate.
3. Fuzz test — When exploring adversarial or untrusted input spaces. One fuzz target can subsume hundreds of hand-crafted "weird input" tests.
4. 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.
Cluster: parse_duration()
- test_parse_seconds → parse_duration("5s") == Duration::from_secs(5)
- test_parse_minutes → parse_duration("3m") == Duration::from_secs(180)
- test_parse_hours → parse_duration("2h") == Duration::from_secs(7200)
- test_parse_zero → parse_duration("0s") == Duration::ZERO
- test_parse_large → parse_duration("999h") == Duration::from_secs(3596400)
- test_parse_invalid_unit → parse_duration("5x") == Err(...)
- test_parse_empty → parse_duration("") == Err(...)
- test_parse_negative → parse_duration("-1s") == Err(...)Step 2: Classify Each Cluster
For each cluster, determine the best consolidation strategy:
Can you state a universal property? → Property-based test. Examples:
- "parse then format roundtrips for all valid inputs"
- "output length is always ≤ input length"
- "sorted output is a permutation of input"
- "encoding never produces invalid UTF-8"
Is it a finite set of (input, expected) with no general property? → Parameterized test (rstest) or table-driven test. Examples:
- Known error codes mapping to messages
- Specific file extensions mapping to MIME types
- Configuration keys mapping to defaults
Are the tests exploring adversarial/edge inputs? → Fuzz test. Examples:
- "doesn't panic on any byte sequence"
- "doesn't allocate more than 10x input size"
Does each test have genuinely unique setup or assertions? → Keep as individual tests. Don't force consolidation.
Step 3: Evaluate Consolidation Candidates
For each cluster, answer:
| Question | If Yes | If No |
|---|---|---|
| Can I state one invariant covering all cases? | proptest | Next question |
| Are all test bodies structurally identical? | rstest | Partial consolidation |
| Do >3 tests share the same assertion pattern? | At minimum rstest | Probably keep individual |
| Would adding a new case require a new function? | Consolidate (adding cases should be trivial) | Fine as-is |
| Do tests differ only in values, not in logic? | Strong consolidation candidate | Keep separate |
Step 4: Choose the Right Tool
Property-Based (proptest) — Best for invariants
Use when: The assertion is about a property that holds regardless of input.
// BEFORE: 6 unit tests
#[test] fn encode_ascii() { assert_eq!(encode("hello"), "aGVsbG8="); }
#[test] fn encode_empty() { assert_eq!(encode(""), ""); }
#[test] fn encode_unicode() { assert_eq!(encode("café"), "Y2Fmw6k="); }
#[test] fn decode_ascii() { assert_eq!(decode("aGVsbG8="), "hello"); }
#[test] fn decode_empty() { assert_eq!(decode(""), ""); }
#[test] fn decode_unicode() { assert_eq!(decode("Y2Fmw6k="), "café"); }
// AFTER: 1 property test (covers infinite inputs)
proptest! {
#[test]
fn roundtrip(input in "\\PC*") {
let encoded = encode(&input);
let decoded = decode(&encoded).unwrap();
prop_assert_eq!(input, decoded);
}
}
// KEEP: encode_empty as anchor (documents empty-input behavior)Parameterized (rstest) — Best for case tables
Use when: You have specific (input, expected) pairs with no general property.
// BEFORE: 7 unit tests
#[test] fn status_200() { assert_eq!(status_text(200), "OK"); }
#[test] fn status_201() { assert_eq!(status_text(201), "Created"); }
#[test] fn status_400() { assert_eq!(status_text(400), "Bad Request"); }
#[test] fn status_404() { assert_eq!(status_text(404), "Not Found"); }
#[test] fn status_500() { assert_eq!(status_text(500), "Internal Server Error"); }
#[test] fn status_unknown() { assert_eq!(status_text(999), "Unknown"); }
#[test] fn status_zero() { assert_eq!(status_text(0), "Unknown"); }
// AFTER: 1 parameterized test
#[rstest]
#[case(200, "OK")]
#[case(201, "Created")]
#[case(400, "Bad Request")]
#[case(404, "Not Found")]
#[case(500, "Internal Server Error")]
#[case(999, "Unknown")]
#[case(0, "Unknown")]
fn status_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] fn parse_null_bytes() { let _ = parse(b"\x00\x00"); }
#[test] fn parse_huge_input() { let _ = parse(&vec![0xFF; 10_000]); }
#[test] fn parse_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 bugsStep 5: Check for Blockers
Before recommending consolidation, verify:
- [ ] Feature gates: proptest requires
stdx-proptest. If all tests move
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 available in eval-harness
(rstest = "0.24" in dev-dependencies). For other crates, add it to [dev-dependencies] in the relevant Cargo.toml.
Step 6: Produce Consolidation Plan
For each cluster, specify:
1. Current state: Number of tests, lines of test code 2. Recommended strategy: Which tool and why 3. What gets removed: Which specific tests are replaced 4. What gets kept: Which tests survive and why 5. New test code: The actual replacement test(s) 6. 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 - Separate test files:
src/stdx/*_tests.rs,src/engine/tests.rs - Property tests: often in the same file, gated with
#[cfg(all(test, feature = "stdx-proptest"))] - Kani proofs:
#[cfg(kani)] mod kani_proofs { ... }or in*_tests.rs - Simulation tests:
tests/simulation/directory
Feature gates
- Property tests:
stdx-proptestfeature - Kani proofs:
kanifeature - Simulation harnesses:
sim-harness,scheduler-sim
Dependencies to add if recommending new tools
- rstest: Add
rstest = "0.24"to[dev-dependencies]in Cargo.toml
(already present in eval-harness)
- proptest: Already available behind
stdx-proptestfeature - 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:**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 = "0.24"` to `[dev-dependencies]` (if rstest recommended)
- [ ] No new dependencies needed (if only proptest/fuzz)
### Migration Order
1. Add proptest for Cluster 1 (highest value: 8 tests → 1)
2. Add rstest for Cluster 3 (6 tests → 1 parameterized)
3. Add rstest for Cluster 5 (4 tests → 1 parameterized)
4. Run full test suite to verify no coverage loss
5. Delete subsumed unit testsDecision 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
Always prefer rstest over table-driven loops. rstest gives you named sub-cases in test output, better failure diagnostics with the exact failing case visible in the test name, and no manual loop/assertion boilerplate. There is no scenario where a hand-rolled for loop over a table is preferable to #[rstest] with #[case] attributes.
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
Related skills
FAQ
When should I keep individual unit tests?
When each test has genuinely unique setup, exercises distinct error paths, or documents a specific bug - the docs call individual tests the last resort but valid then.
Why prefer rstest over table-driven tests?
The docs say rstest gives named sub-cases in test output, better failure diagnostics, and no manual loop boilerplate.