---
name: shrink-tests
description: Use when a TDD ladder has reached green and its tests are about to merge, when a branch diff adds tests that only restate the code they drove, or when the test suite prevents refactoring instead of catching regressions. Triggers on "these tests only repeat the code", "this test fails after every rename", "drop the redundant tests", "the suite blocks every refactor". Do not use it to delete failing tests and make a red suite green.
---
# Shrink Tests
## Overview
A TDD test does two jobs. While the code is red, the test **specifies**: it
states what the code must do. After the code is green, the test **guards**: it
catches a later change that breaks the required behaviour.
Many tests stop after the first job. The author writes such a test while reading
the implementation, so the test holds a copy of that implementation. This skill
calls that test a **mirror**. A mirror cannot fail for a reason worth knowing,
and every later refactor must still update it.
Shrinking deletes the mirrors and keeps the guards. A shrunk suite fails when a
change breaks the required behaviour. It stays silent when a change only
rewrites how the code supplies that behaviour.
A shrink run never changes behaviour. It only deletes tests and merges tests.
## Default Scope
Review the tests that this branch added or changed:
```bash
git diff --name-only ...HEAD -- '*test*' '*spec*'
git diff ...HEAD --
```
The glob is a start, not the definition of the scope. The scope is every test
that this branch changed, at any location. The glob misses three common cases:
a Rust `#[cfg(test)] mod tests` block inside the production file, a Python
doctest, and a `FooTest.java` that the lowercase pattern does not match. If the
pathspec returns no file, read the full branch diff. Do not report that the
branch added no test until you do this.
This set is the output of the most recent ladder, and you still know the intent
of each test in it. A review of the full suite is a different task. Do that task
only on request. See "Whole-Suite Mode".
## The Criterion — Two Layers
### Layer 1 — Coupling count (screen)
For each test, ask this question: **which independent places in the code must I
edit to make this test fail?** Name each place.
- **Two or more places → Keep. Stop here.** The test holds an agreement that no
single place states. If you delete the test, no record of the agreement
remains.
- **Exactly one place → candidate.** Go to Layer 2.
Layer 1 deletes no test. Layer 1 only selects the tests to examine.
### Layer 2 — Derivation (verdict)
For each candidate from Layer 1, ask this question: **can I write this test from
the requirement alone, without reading the implementation?**
Hide the implementation. Read the ADR line, the docstring, the defect report, or
the interface contract. Then write the test again from those sources.
- **You can write it again → Keep.** The test is an independent statement of the
requirement. A boundary test on a pure function couples to one place, and it
is still a Keep. The test states the permitted range from the requirement
side. The code states the same range from the other side. Two independent
statements of one range are the reason to keep the test.
- **You cannot write it again → Delete or Merge.** The author copied the test
from the code. The test holds no information that the code does not hold, so
it cannot fail for a reason worth knowing.
The same question in short form: **if this test fails, does it tell me anything
that the diff does not tell me?** If the only message is "you changed the line
that you just changed", the test is a mirror of that line.
## Archetypes
### Delete — mirrors
- **Constant mirror** — `assert MAX_RETRIES == 3`. The constant is the code. The
assertion is a second copy of the constant in a second file.
- **Attribute mirror** — a class attribute, a dataclass default, or an enum
member list, compared against its own literal.
- **Transcription set** — one test for each branch of one function. The name
quotes the implementation, for example `test_returns_early_when_cache_hit`.
The author wrote the set while reading the function.
- **Call-shape mock** — `assert_called_once_with(a, b)`, where the author copied
the argument list from the call site. The test pins the call, not the result.
- **Trivial surface** — a getter, a setter, `__repr__`, a delegation that holds
no logic, or a constructor that only assigns.
- **Literal twin** — the same behaviour as an adjacent test, with a different
input value and no new region of behaviour.
### Merge — keep the case, delete the duplication
- Several constant assertions or attribute assertions serve one invariant.
Replace them with one table test of that invariant. Examples: *every declared
lifecycle value is in `LIFECYCLE`*, *every registered crawler name resolves to
a settings class*. The table test fails when a change breaks the relationship.
It does not fail when a person changes a number on purpose.
- Several one-method tests cover one class. Replace them with one test that runs
a realistic sequence of calls and asserts the final state. The replacement
also catches order-dependent defects that no single-method test can reach.
- Several edge-case tests overlap. Replace them with one boundary table that
holds the cases as parameters.
Select Merge, not Delete, whenever a Delete removes the last cover of a real
case.
### Keep — delete none of these without a named replacement
- **Cross-location contracts** — a producer and a consumer that must agree, two
mirrored copies that must stay equal, a serialized format that another module
reads, or an exit code that one place raises and another place maps.
- **Multi-method sequences** — one method writes the state, and another method
reads it.
- **Boundary and range** — empty, `None`, zero, one, maximum, off-by-one,
timezone, unicode, overflow, and duplicate.
- **Regression tests for fixed defects** — the test records a fact that the code
cannot state, which is that this case was wrong before.
- Any test whose failure reports a broken requirement, not a changed value.
## Procedure
1. **Start green and clean.** Run the suite. Record the count of passing tests.
Record a **per-line coverage artifact**: `coverage xml`, `lcov.info`, or the
equivalent. Do not record only a summary percentage. Never shrink while the
suite is red. Never shrink while the tree is dirty.
2. **Inventory.** Make one row for each added test and each changed test. Each
row holds `file:line`, the test name, and the places that the test pins.
3. **Screen.** Apply Layer 1 to every row.
4. **Judge.** Apply Layer 2 to the one-place rows only.
5. **Propose, then stop.** Show the table. Give each row a verdict: Delete,
Merge, or Keep. Give one sentence of reasoning for each Delete row and each
Merge row. Write out each merged test in full. Make no edit yet. Wait for
approval.
6. **Apply the merges first. Apply the deletions second.** Measure coverage
again between the two passes, and keep that measurement. The Step-7 gate
reports a line that lost its cover in either order, but it cannot report
which pass removed the cover. The measurement between the passes supplies
that fact. Step 7 uses it.
7. **Verify line by line.** Confirm that the suite is green and that the test
count is lower. Compare the final coverage artifact against the Step-1
baseline **for each line and each branch**. No line that had cover before the
shrink may have zero cover after it. A summary percentage cannot show this,
because a percentage can stay level while one line loses its only cover. When
a line fails this gate, read the Step-6 measurement. It gives one of two
answers. A line with cover after the merges and no cover after the deletions
names the deleted row that removed the cover. Restore that test, or add its
case to a test that remains. A line with no cover after the merges names a
case that the merged table omits. Add that case to the table, because the
row that held it is already gone.
8. **Commit the shrink alone.** Use `test(): drop mirror tests for
`. A shrink never shares a commit with a behaviour change. A later
`git revert` then restores the tests and changes no production code.
A Keep row needs no reasoning. A Delete row and a Merge row each need one
sentence.
## Safety Rails
- **Never shrink while the suite is red.** A failing test is not a shrink
candidate. If you delete it, you hide a real failure and record the result as
cleanup. Repair the test instead. If the test must go, delete it as a separate
decision and state the reason.
- **Never put a shrink and a behaviour change in one commit.**
- **Read every candidate to its last line.** A test can open with three mirror
assertions and close with a real contract assertion. That test is a Keep, or a
Merge that retains the closing assertion.
- **"Annoying" is not a verdict.** A test that fails after every rename is one
of two things. It is a mirror, so delete it. Or it is a contract that is
pinned to a name, which means the design couples to that name. In the second
case, repair the design and keep the test.
- **Stay inside the diff.** In the default scope, do not touch a test that the
branch did not change, even when that test looks like a mirror.
## Red Flags — STOP
- Coverage decreased, and the run continued.
- The suite changed from red to green across the shrink.
- The delete list holds the last test of an error path or an exception branch.
- The delete list holds a test whose origin you cannot name.
- You plan to delete tests now and to add them again later if they are needed.
Nobody adds them again.
## Common Rationalizations
| Excuse | Reality |
|---|---|
| "The test is short, so it costs nothing to keep." | The cost is not run time. Each mirror is a second place to edit at the next refactor. The edit is mechanical, so the author makes it without thought. A suite in that state stops catching defects. |
| "It couples to one place, so the rule says delete it." | The rule says examine it. Layer 2 gives the verdict. A boundary test on one function stays. |
| "It covers a public API, so it must be a contract." | Public and agreed are not the same. If no second place depends on the value, the test still mirrors one line. |
| "Coverage decreases, but that path is clearly correct." | Then the path has no cover and no reader. Add its case to a test that remains, instead of deleting the case. |
| "I will shrink while I finish the feature." | A shrink on a dirty tree hides which change caused which failure. Complete the feature, reach green, then shrink. |
| "The merged table test is harder to read than five assertions." | The five assertions state a value five times. The table states the relationship once, and the relationship is the part that can break. |
## Whole-Suite Mode
Use this mode only on an explicit request. The criterion does not change. Only
the sequence changes.
Do not inventory the full suite in one pass. Rank the modules first, by two
signals that predict mirrors:
- a test file that changed together with one production file across the history
(co-change in `git log --name-only`)
- test names that quote implementation details instead of requirements
Then take one module at a time through the full procedure: propose, apply,
verify, commit. Nobody can review a bulk delete that spans modules, and
`git bisect` cannot isolate it.
## Related Skills
- `tdd-checkbox-plans` — the upstream skill. The ladder writes these tests.
Shrink them after the last task reaches green.
- `verification-before-completion` — the suite result and the coverage
comparison in Step 7 are the completion evidence for a shrink.
- `finishing-a-development-branch` — shrink before the merge decision, not after
it.
- `reclaim-code-entropy` — the same evidence-first method, applied to production
code instead of test code.