ConstructiCat Logo
CodeBust.
Browse section ▾

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 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 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:

  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 un Obscure Test).
  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: 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-functionsLas funciones no deben tener implementaciones idénticas
  • eslint-sonarjs no-duplicate-stringLos literales de cadena no deben duplicarse
  • sonar javascript:S4144Las funciones no deben tener implementaciones idénticas
  • pmd cpdCopy/Paste Detector (CPD) — bloques de código duplicados