Test maintenance
Test code is production code with a longer memory: it outlives features and
gets read far more than it is written. Left untended it accretes copy-pasted
setup, tests that assert nothing, and duplicates that all break together on one
change. Maintaining it means the same moves you make in application code, plus
one nerve most people lack: deleting a test that has stopped paying for its
runtime.
Method
- Extract setup into named builders, not shared mutable state. Replace
repeated arrange blocks with a factory or builder
(
makeUser({suspended: true})) that returns fresh objects. Avoid a shared
setUp that mutates instance fields across tests, which couples them and
breeds order-dependent flakiness.
- Dedupe the assertion, keep the intent visible. Pull a repeated
multi-line check into a custom matcher or a helper
(
assertValidInvoice(inv)), but leave the input and the expected outcome in
the test body. A test whose meaning now lives entirely in a helper is
unreadable at the call site.
- Delete tests that no longer earn their runtime. Remove tests that assert
a deleted feature, duplicate another test's coverage exactly, or only
restate the implementation. Confirm the deletion is safe with a coverage or
mutation run: if killing the test does not drop either number, it was not
pulling weight.
- Fix or quarantine flaky tests on sight, never retry blindly. A test that
fails intermittently is worse than no test: it trains the team to ignore
red. Diagnose the nondeterminism (time, ordering, shared state), fix it, or
move it to a quarantined lane with an owner and a deadline rather than
wrapping it in a retry.
- Refactor tests and production in separate commits. When you change both,
commit the test refactor while behavior is green first, then the behavior
change. Mixing them means a red suite cannot tell you whether the refactor or
the feature broke.
- Keep one behavior per test and one reason to fail. Split a test that
guards several behaviors so a failure names one cause. Rename tests whose
description drifted from what they now check, since a lying test name costs
more than no name.
Signals
- Does changing one production behavior break exactly the tests about that
behavior, and no unrelated others?
- Can you delete a test and immediately tell from coverage or mutation whether
it mattered?
- Is there a flaky test currently failing that everyone has silently agreed to
ignore?
Boundaries
This keeps an existing suite healthy; it does not decide the initial mix of
tests, which is testing-strategy, or how to design a single test, which is
unit-test-design. Deleting a test is a judgment call: when coverage cannot tell
you whether it matters, keep it and ask a reviewer.
1---2name: test-maintenance3description: Refactor test code with the same care as production: extract helpers, remove duplication, and delete tests that no longer earn their place. Use when the test suite has become slow to change, repetitive, or full of tests nobody trusts.4---56# Test maintenance78Test code is production code with a longer memory: it outlives features and9gets read far more than it is written. Left untended it accretes copy-pasted10setup, tests that assert nothing, and duplicates that all break together on one11change. Maintaining it means the same moves you make in application code, plus12one nerve most people lack: deleting a test that has stopped paying for its13runtime.1415## Method16171. **Extract setup into named builders, not shared mutable state.** Replace18 repeated arrange blocks with a factory or builder19 (`makeUser({suspended: true})`) that returns fresh objects. Avoid a shared20 `setUp` that mutates instance fields across tests, which couples them and21 breeds order-dependent flakiness.222. **Dedupe the assertion, keep the intent visible.** Pull a repeated23 multi-line check into a custom matcher or a helper24 (`assertValidInvoice(inv)`), but leave the input and the expected outcome in25 the test body. A test whose meaning now lives entirely in a helper is26 unreadable at the call site.273. **Delete tests that no longer earn their runtime.** Remove tests that assert28 a deleted feature, duplicate another test's coverage exactly, or only29 restate the implementation. Confirm the deletion is safe with a coverage or30 mutation run: if killing the test does not drop either number, it was not31 pulling weight.324. **Fix or quarantine flaky tests on sight, never retry blindly.** A test that33 fails intermittently is worse than no test: it trains the team to ignore34 red. Diagnose the nondeterminism (time, ordering, shared state), fix it, or35 move it to a quarantined lane with an owner and a deadline rather than36 wrapping it in a retry.375. **Refactor tests and production in separate commits.** When you change both,38 commit the test refactor while behavior is green first, then the behavior39 change. Mixing them means a red suite cannot tell you whether the refactor or40 the feature broke.416. **Keep one behavior per test and one reason to fail.** Split a test that42 guards several behaviors so a failure names one cause. Rename tests whose43 description drifted from what they now check, since a lying test name costs44 more than no name.4546## Signals4748- Does changing one production behavior break exactly the tests about that49 behavior, and no unrelated others?50- Can you delete a test and immediately tell from coverage or mutation whether51 it mattered?52- Is there a flaky test currently failing that everyone has silently agreed to53 ignore?5455## Boundaries5657This keeps an existing suite healthy; it does not decide the initial mix of58tests, which is testing-strategy, or how to design a single test, which is59unit-test-design. Deleting a test is a judgment call: when coverage cannot tell60you whether it matters, keep it and ask a reviewer.