Duplicación de código en los tests.
La duplicación de código en los tests se da cuando el mismo código de configuración, acción o aserción se copia y pega en muchos tests, de modo que un solo cambio obliga a editar en muchos lugares y los tests se pudren convirtiéndose en copias frágiles casi idénticas.
##Signs and Symptoms
Reconoces este mal olor cuando los tests parecen escritos a copia y pega en lugar de mediante reutilización:
- La misma construcción de fixture/objeto se reconstruye literalmente al principio de un test tras otro.
- Se repiten en varios tests secuencias de aserciones idénticas (las mismas 3-4 llamadas a
expecten el mismo orden). - Los nuevos tests son, evidentemente, clones de uno antiguo con una sola línea retocada.
- Los mismos literales mágicos (IDs, URLs, fechas, cadenas de error) aparecen una y otra vez.
- Un cambio en un constructor o en la firma de una API rompe docenas de tests a la vez (cirugía de escopeta).
- Ves tests casi duplicados que solo difieren en los valores de entrada/esperados: un claro candidato para un test parametrizado/de tabla.
test('flight can be cancelled', () => {
const airport = new Airport('YYC', 'Calgary'); // duplicado
const flight = new Flight('AC123', airport, new Date('2026-06-01T10:00')); // duplicado
flight.cancel();
expect(flight.status).toBe('CANCELLED');
});
test('flight can be delayed', () => {
const airport = new Airport('YYC', 'Calgary'); // copia y pega
const flight = new Flight('AC123', airport, new Date('2026-06-01T10:00')); // copia y pega
flight.delay(30);
expect(flight.status).toBe('DELAYED');
});
##Reasons for the Problem
Por qué ocurre
- Copiar y pegar el test anterior es la forma más rápida de escribir el siguiente.
- Los tests se tratan como «ciudadanos de segunda clase»: no se refactorizan ni se les exige el mismo estándar DRY que al código de producción.
- No existen fixtures compartidos, Creation Methods ni constructores de datos de test, y los autores no están familiarizados con los tests parametrizados o basados en tablas.
Por qué es perjudicial
- Mantenibilidad: Meszaros vincula este mal olor directamente con el Fragile Test: cuando «las mismas secuencias de código aparecen muchas veces en muchos tests», un único cambio en producción implica editar lo mismo en N lugares. El coste de mantenimiento escala con el número de copias, no con el número de comportamientos distintos.
- Legibilidad: el boilerplate repetido entierra la única línea que realmente hace distinto a cada test, así que los lectores no pueden ver rápidamente qué se está verificando.
- Fiabilidad: copiar y pegar invita a errores de copiar y pegar, y las correcciones se aplican a una copia pero no a sus hermanas, dejando tests inconsistentes y contradictorios.
- Falsa confianza: una aserción defectuosa que se duplicó ahora es incorrecta en muchos lugares a la vez, y los tests clonados van a la deriva en silencio hasta que dejan de ejercitar lo que sus nombres afirman.
Advertencia: DRY frente a DAMP: los tests también valoran ser Descriptive And Meaningful Phrases (frases descriptivas y significativas). No abstraigas en exceso hasta el punto de que un lector tenga que perseguir helpers para entender un test. Extrae la duplicación genuina que revela la intención; mantén el detalle esencial de cada test visible y local.
##Treatment
Elimina la duplicación incidental manteniendo evidente la esencia de cada test:
- Extrae utilidades de test / Creation Methods (Object Mother, Test Data Builder) para la construcción de objetos repetida, de modo que cada test nombre solo los valores que le importan.
- Usa
beforeEach/ Implicit Setup para el contexto que es genuinamente compartido y relevante para todos los tests del bloque, pero evita ocultar el estado del que depende un test (eso cambia la duplicación por un Obscure Test). - Extrae Custom Assertions / helpers de verificación para las secuencias de aserciones de varios pasos que se repiten, idealmente verificando una única condición lógica.
- Colapsa los tests casi idénticos en tests parametrizados / de tabla (
it.each/test.each) para que los pares de entrada + salida esperada vivan en una sola tabla. - Reemplaza los literales mágicos duplicados por constantes con nombre o valores por defecto del builder.
Antes: construcción duplicada y tres tests casi idénticos:
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);
});
Después: un Creation Method elimina la duplicación de configuración y una tabla reemplaza los clones:
// Creation Method compartido: los tests indican solo los overrides que importan
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);
});
Ejecuta un detector de copia y pega (más abajo) sobre tus fuentes de test y, a continuación, refactoriza primero los bloques más grandes/más repetidos.
##Detected by
- eslint-sonarjs no-identical-functions — Las funciones no deben tener implementaciones idénticas
- eslint-sonarjs no-duplicate-string — Los literales de cadena no deben duplicarse
- sonar javascript:S4144 — Las funciones no deben tener implementaciones idénticas
- pmd cpd — Copy/Paste Detector (CPD) — bloques de código duplicados