Logotipo de ConstructiCat
CodeBust.
Explorar la sección ▾

Solo para los tests.

Las clases de producción contienen miembros que solo existen para los tests —getters de estado, métodos reset(), miembros con visibilidad ampliada y marcados con @VisibleForTesting—, lo que contamina la API real e invita a tests que verifican detalles internos en lugar de comportamiento.

##Signos y síntomas

Encuentras miembros en el código de producción cuyos únicos llamadores viven en archivos de test. Señales reveladoras:

  • Métodos, getters/setters, exports o parámetros de constructor usados exclusivamente desde *.test.ts / *.spec.ts.
  • Nombres o marcadores con aroma a test: getStateForTest, resetForTesting, __getInternal, FTO_*, forTest, comentarios como // only used by tests, o anotaciones como @VisibleForTesting / @TestOnly / @internal.
  • Visibilidad relajada (un campo vuelto public/exportado, un #private convertido en protected) únicamente para que un test pueda alcanzar el estado interno.
  • Mutadores solo para tests —reset(), setClockForTest(...), setRandomForTest(...)— añadidos solo porque un test necesitaba rebobinar o dirigir el objeto, no porque los use ningún llamador de producción. (Las ramas que dependen de un indicador de «modo test», las dependencias exclusivas de los tests y los métodos de igualdad añadidos solo para las aserciones pertenecen a la Lógica de Prueba en Producción.)
// payment-service.ts  (código de PRODUCCIÓN)
export class PaymentService {
  #ledger: Entry[] = [];
  private now: () => Date = () => new Date();

  charge(amount: number) { /* ... */ }

  // Nada en producción llama nunca a estos; solo lo hacen los tests:
  getLedgerForTest() { return this.#ledger; }          // expone detalles internos
  setClockForTest(now: () => Date) { this.now = now; } // costura solo para tests
  FTO_reset() { this.#ledger = []; }                   // "For Tests Only"
}

Una comprobación rápida: un grep -rn 'forTest\|ForTesting\|FTO_' src/ que devuelva coincidencias en el código fuente de producción, o una pasada de código muerto (p. ej. Knip en modo producción) que informe de un export como no usado mientras un test claramente lo importa.

##Causas del problema

Por qué ocurre

  • Adaptar tests a código no testeable. Cuando el código heredado no se diseñó para ser testeable, la forma más rápida de hacer una aserción sobre un resultado es abrir un agujero en el SUT y leer sus detalles internos. El nombre del olor (For Testers Only) procede de van Deursen et al. (2001), que describieron clases de producción con métodos usados solo por los tests; más tarde Meszaros recogió el mismo problema como For Tests Only, una de las causas de la Lógica de Prueba en Producción.
  • APIs asimétricas. Los clientes reales usan un objeto de una manera (escritura); los tests lo usan de forma simétrica (escriben y luego leen de vuelta para verificar), así que los tests «necesitan» accesores que ningún llamador de producción necesita.
  • Presión de plazos. Añadir una puerta trasera es más barato en el momento que refactorizar hacia un diseño en el que el comportamiento sea observable a través del contrato público.

Por qué es perjudicial

  • Legibilidad. La API pública deja de decir la verdad: los responsables del mantenimiento no pueden distinguir el contrato real del andamiaje de los tests, y cada lector tiene que preguntarse «¿se usa de verdad este método?».
  • Encapsulación y fiabilidad. El estado interno se vuelve accesible y mutable en el código que se distribuye. Una llamada perdida a FTO_reset() o un setter filtrado pueden corromper el estado en producción; la superficie adicional también infla el bundle y amplía la superficie de ataque.
  • Falsa confianza. Los tests que hurgan en el estado privado hacen aserciones sobre la implementación, no sobre el comportamiento. Pueden seguir en verde mientras el contrato público está roto, y se rompen ante refactorizaciones inofensivas: tests frágiles que prueban lo que no deben.
  • Mantenibilidad. Los miembros solo para tests parecen código muerto pero no pueden eliminarse; cada cambio tiene que tener en cuenta llamadores fantasma, y el mal olor tiende a multiplicarse a medida que más tests reutilizan la puerta trasera.

##Tratamiento

Trata la puerta trasera como una señal de diseño, no como un detalle del fixture.

  1. Prueba primero a través del comportamiento observable. Haz aserciones sobre valores de retorno, eventos emitidos, salida persistida o interacciones con colaboradores (mediante dobles de test) en lugar de hurgar en los detalles internos. La mayoría de los accesores getXForTest desaparecen en cuanto verificas lo que el objeto hace, no lo que contiene.
  2. Usa una Test-Specific Subclass cuando realmente necesites acceso interno. Extiende la clase en el test para exponer un miembro protected, en lugar de ampliar la visibilidad en producción.
  3. Haz que las costuras formen parte del diseño real, no de escotillas solo para tests. Inyectar un reloj o un RNG a través del constructor normal es una inyección de dependencias legítima; un mutador setClockForTest() es un mal olor. Si una costura solo tiene sentido para los tests, lleva el comportamiento a un Strategy/Null Object que producción instale por defecto y el test sustituya.
  4. Si la exposición es realmente inevitable, etiquétala a gritos y vállala. Márcala con @VisibleForTesting / @internal (o una convención de nombres FTO_) y haz cumplir que el código de producción nunca la llame (véanse los detectores). Una costura marcada y protegida es mejor que una silenciosa.
  5. Caza a los infractores existentes. Haz grep de forTest/ForTesting/FTO_ y ejecuta una herramienta de uso/código muerto como Knip en modo producción para sacar a la luz los exports referenciados únicamente por archivos de test.
// ANTES — producción arrastra un accesor solo para tests
export class Cart {
  #items: Item[] = [];
  add(i: Item) { this.#items.push(i); }
  getItemsForTest() { return this.#items; } // solo para los testers
}
// test
expect(cart.getItemsForTest()).toHaveLength(1);
// DESPUÉS — verifica el comportamiento a través del contrato real
export class Cart {
  #items: Item[] = [];
  add(i: Item) { this.#items.push(i); }
  get count() { return this.#items.length; }
  get total() { return this.#items.reduce((s, i) => s + i.price, 0); }
}
// test
cart.add({ price: 10 });
expect(cart.count).toBe(1);
expect(cart.total).toBe(10);

Si aún necesitas acceso interno, restringe la costura a los tests con una subclase en lugar de abrirla al mundo entero:

// la producción se mantiene limpia: queue es protected, no public
export class Scheduler {
  protected queue: Job[] = [];
  enqueue(j: Job) { this.queue.push(j); }
}
// solo en el archivo de test
class TestScheduler extends Scheduler {
  peek() { return this.queue; }
}

##Detectado por

  • sonar java:S5803 — Los miembros «@VisibleForTesting» no deben usarse desde el código de producción