Test Code Duplication.
Test Code Duplication is when the same setup, action, or assertion code is copy-pasted across many tests, so a single change forces edits in many places and tests rot into fragile, near-identical copies.
##Signs and Symptoms
You recognize this smell when tests look like they were written with copy-paste rather than reuse:
- The same fixture/object construction is rebuilt verbatim at the top of test after test.
- Identical assertion sequences (the same 3–4
expectcalls in the same order) recur across tests. - New tests are obviously cloned from an old one with a single line tweaked.
- The same magic literals (IDs, URLs, dates, error strings) appear over and over.
- A change to one constructor or API signature breaks dozens of tests at once (shotgun surgery).
- You see near-duplicate tests that differ only in input/expected values — a clear candidate for a parameterized/table test.
test('flight can be cancelled', () => {
const airport = new Airport('YYC', 'Calgary'); // duplicated
const flight = new Flight('AC123', airport, new Date('2026-06-01T10:00')); // duplicated
flight.cancel();
expect(flight.status).toBe('CANCELLED');
});
test('flight can be delayed', () => {
const airport = new Airport('YYC', 'Calgary'); // copy-paste
const flight = new Flight('AC123', airport, new Date('2026-06-01T10:00')); // copy-paste
flight.delay(30);
expect(flight.status).toBe('DELAYED');
});
##Reasons for the Problem
Why it happens
- Copy-pasting the previous test is the fastest way to write the next one.
- Tests are treated as a "second-class citizen" — not refactored or held to the same DRY standard as production code.
- No shared fixtures, Creation Methods, or test-data builders exist, and authors are unfamiliar with parameterized/table-driven tests.
Why it hurts
- Maintainability: Meszaros ties this smell directly to Fragile Test — when "the same code sequences appear many times in many tests," one production change means editing the same thing in N places. Maintenance cost scales with the number of copies, not with the number of distinct behaviors.
- Readability: repeated boilerplate buries the one line that actually makes each test different, so readers can't quickly see what is being verified.
- Reliability: copy-paste invites copy-paste errors, and fixes get applied to one copy but not its siblings, leaving inconsistent, contradictory tests.
- False confidence: a flawed assertion that was duplicated is now wrong in many places at once, and cloned tests silently drift until they no longer exercise what their names claim.
Caveat — DRY vs DAMP: tests also value being Descriptive And Meaningful Phrases. Don't over-abstract to the point that a reader must chase helpers to understand a test. Extract genuine, intent-revealing duplication; keep the essential, per-test detail visible and local.
##Treatment
Eliminate the incidental duplication while keeping each test's essence obvious:
- Extract Test Utility / Creation Methods (Object Mother, Test Data Builder) for repeated object construction, so each test names only the values it cares about.
- Use
beforeEach/ Implicit Setup for context that is genuinely shared and relevant to every test in the block — but avoid hiding state a test depends on (that trades duplication for an Obscure Test). - Extract Custom Assertions / verification helpers for recurring multi-step assertion sequences, ideally verifying one logical condition.
- Collapse near-identical tests into Parameterized / table-driven tests (
it.each/test.each) so input + expected output pairs live in one table. - Replace duplicated magic literals with named constants or builder defaults.
Before — duplicated construction and three near-identical tests:
test('rejects negative amount', () => {
expect(() => validateAmount(-1)).toThrow(RangeError);
});
test('rejects zero amount', () => {
expect(() => validateAmount(0)).toThrow(RangeError);
});
test('rejects NaN amount', () => {
expect(() => validateAmount(NaN)).toThrow(RangeError);
});
After — a Creation Method removes setup duplication, and a table replaces the clones:
// shared Creation Method: tests state only the overrides that matter
const aFlight = (overrides = {}) =>
new Flight('AC123', new Airport('YYC', 'Calgary'),
new Date('2026-06-01T10:00'), overrides);
it.each([-1, 0, NaN])('rejects invalid amount %p', (amount) => {
expect(() => validateAmount(amount)).toThrow(RangeError);
});
Run a copy-paste detector (below) over your test sources, then refactor the largest/most-repeated blocks first.
##Detected by
- eslint-sonarjs no-identical-functions — Functions should not have identical implementations
- eslint-sonarjs no-duplicate-string — String literals should not be duplicated
- sonar javascript:S4144 — Functions should not have identical implementations
- pmd cpd — Copy/Paste Detector (CPD) — duplicated code blocks