skills/testing/test-deduplicator/SKILL.md
Finds and removes redundant tests — tests that cover the same code, kill the same mutants, or assert the same behavior — to shrink suite runtime without losing coverage. Use when the test suite is slow, when tests have accumulated over years of copy-paste, or when CI costs are too high.
npx skillsauth add santosomar/general-secure-coding-agent-skills test-deduplicatorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ten tests cover the same branch. Nine of them can go. But which nine? The one you keep is the one that covers something the others don't.
| Sameness level | Evidence | Deletion confidence | | ----------------------------- | ------------------------------------------------------------ | ------------------- | | Textual clone | Near-identical code, one value different | Low — might test different boundaries | | Same coverage | Per-test coverage sets are equal | Medium | | Same coverage, subsumes | Test A's coverage ⊇ Test B's | High — B is redundant | | Same mutants killed | Mutation testing: A and B kill identical mutant sets | High | | Same assertions, same inputs | Literally the same test, different name | Very high — copy-paste accident |
Coverage subsumption is the key signal. If test A covers everything test B covers and more, B adds nothing.
# Python — run each test in isolation, collect coverage
pytest --cov=src --cov-context=test --cov-report=
coverage json -o coverage.json
# coverage.json now has per-context (per-test) line data
# Java — JaCoCo per-test is harder; use test-impact tools or:
# Run suite once, instrument to log (test_name, covered_line) pairs
Output: {test_name: set(covered_lines)}
# A subsumes B if coverage[A] >= coverage[B] (superset)
subsumed = []
tests = sorted(coverage, key=lambda t: len(coverage[t]), reverse=True)
for i, a in enumerate(tests):
for b in tests[i+1:]:
if coverage[a] >= coverage[b]: # a's coverage is a superset
subsumed.append((b, a)) # b is subsumed by a
This is O(n²) set comparisons. Fine for a few thousand tests. For tens of thousands, sort by size and prune.
Subsumption alone misses partial overlap. For the minimum set of tests with the same total coverage, use greedy set cover:
covered = set()
keep = []
remaining = sorted(coverage, key=lambda t: len(coverage[t]), reverse=True)
total = set().union(*coverage.values())
for t in remaining:
new_lines = coverage[t] - covered
if new_lines:
keep.append(t)
covered |= coverage[t]
if covered == total:
break
delete_candidates = set(coverage) - set(keep)
Greedy isn't optimal (set cover is NP-hard) but it's within ln(n) of optimal and runs in seconds.
Coverage equivalence ≠ behavioral equivalence. Two tests can cover the same lines but assert different things:
def test_parse_int():
assert parse("42") == 42 # covers lines 10-15
def test_parse_int_leading_zero():
assert parse("042") == 42 # covers lines 10-15 — same coverage!
Same coverage. Different inputs. The second might catch a bug the first doesn't (octal interpretation, anyone?).
Before deleting, check mutation-kill equivalence: run mutation testing (→ mutation-test-suite-optimizer) on the delete candidates. If a candidate kills a mutant no keeper kills, it's not redundant.
Often the "duplicates" are value variations:
def test_discount_gold(): assert discount(100, "gold") == 80
def test_discount_silver(): assert discount(100, "silver") == 90
def test_discount_bronze(): assert discount(100, "bronze") == 95
def test_discount_none(): assert discount(100, "none") == 100
Four tests, near-identical structure. Don't delete three — parametrize:
@pytest.mark.parametrize("tier,expected", [
("gold", 80), ("silver", 90), ("bronze", 95), ("none", 100),
])
def test_discount(tier, expected):
assert discount(100, tier) == expected
Same coverage. Same assertions. One-quarter the code. Still four test cases.
| Pattern | Why not redundant | | -------------------------------------------------- | ----------------------------------------- | | Same function, one mocks DB, one hits real DB | Unit vs integration — different failure modes | | Same logic, different input sizes (1 vs 10000) | The big one catches O(n²) performance bugs | | Same coverage, one asserts exception message | Error-path granularity | | Flaky test + reliable test covering same thing | Delete the flaky one, keep the reliable one — but this is a flake fix, not dedup |
test_empty_list_returns_empty is worth keeping for the name, even if test_various_inputs also covers it.## Suite before
Tests: <N> Runtime: <s> Coverage: <%> line, <%> branch
## Subsumption
| Subsumed test | Subsumed by | Coverage delta |
| ------------- | ----------- | -------------- |
## Greedy minimum cover
Keep: <N> tests → same total coverage
Delete candidates: <M> tests
## Mutation verification
| Candidate | Mutants only this test kills | Keep? |
| --------- | ---------------------------- | ----- |
## Parametrization opportunities
| Tests | Merged into | Cases |
| ----- | ----------- | ----- |
## Final
| Action | Count |
| ------ | ----- |
| Delete (fully subsumed, no unique mutants) | <N> |
| Parametrize | <N> groups → <M> tests |
| Keep (looked redundant, isn't) | <N> |
## After
Tests: <N> Runtime: <s> Coverage: <%> (unchanged) Mutation score: <%> (unchanged)
development
Extracts human-readable pseudocode from a verified formal artifact (Dafny, Lean, TLA+) while preserving the verified properties as annotations, so the proof-carrying logic can be reimplemented in a production language. Use when porting verified code to an unverified target, when documenting what a formal spec actually does, or when handing a verified algorithm to an implementer.
development
Translates natural-language or pseudocode descriptions of concurrent and distributed systems into TLA+ specifications ready for the TLC model checker. Identifies state variables, actions, type invariants, safety properties, and liveness properties from the description. Use when formalizing a protocol, when the user describes a distributed algorithm to verify, when designing a consensus or locking scheme, or when starting formal verification of a concurrent system.
testing
Reduces a TLA+ model so TLC can actually check it — shrinks constants, adds state constraints, abstracts data, or applies symmetry — when the state space is too large to enumerate. Use when TLC runs out of memory, when checking takes hours, or when a spec works at N=2 and you need confidence at larger scale.
development
TLA+-specific instance of model-guided repair — reads a TLC error trace, identifies the enabling condition that should have been false, strengthens the corresponding action, and maps the fix to source code. Use when TLC reports an invariant violation or deadlock and you have the code-to-TLA+ mapping from extraction.