Logotipo de ConstructiCat
CodeBust.
Explorar la sección ▾

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.

##Signos y síntomas

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 expect en 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 a tiros).
  • 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');
});

##Causas del problema

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.

##Tratamiento

Elimina la duplicación incidental manteniendo evidente la esencia de cada test:

  1. 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.
  2. 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 una prueba oscura).
  3. 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.
  4. 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.
  5. Reemplaza los literales mágicos duplicados por constantes con nombre o valores por defecto del builder.

Antes: la construcción duplicada de Flight de arriba, más tres tests casi idénticos:

// la misma construcción en cada test
test('flight can be cancelled', () => {
  const airport = new Airport('YYC', 'Calgary');
  const flight  = new Flight('AC123', airport, new Date('2026-06-01T10:00'));
  flight.cancel();
  expect(flight.status).toBe('CANCELLED');
});

test('flight can be delayed', () => {
  const airport = new Airport('YYC', 'Calgary');
  const flight  = new Flight('AC123', airport, new Date('2026-06-01T10:00'));
  flight.delay(30);
  expect(flight.status).toBe('DELAYED');
});

// …y clones que solo difieren en la entrada
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: los tests llaman al Creation Method aFlight() y una tabla reemplaza los clones:

// Creation Method: todo con valores por defecto; cada test sobrescribe solo lo que le importa
const aFlight = ({
  number = 'AC123',
  origin = new Airport('YYC', 'Calgary'),
  departure = new Date('2026-06-01T10:00'),
} = {}) => new Flight(number, origin, departure);

test('flight can be cancelled', () => {
  const flight = aFlight();
  flight.cancel();
  expect(flight.status).toBe('CANCELLED');
});

test('flight can be delayed', () => {
  const flight = aFlight();
  flight.delay(30);
  expect(flight.status).toBe('DELAYED');
});

// un test parametrizado reemplaza a los clones
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.

##Detectado por