Solo para los tests.
El código de producción contiene métodos, accesores de estado o costuras (seams) que existen únicamente para que los usen los tests, contaminando la API real e invitando a tests que verifican detalles internos en lugar de comportamiento.
##Signs and Symptoms
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#privateconvertido enprotected) únicamente para que un test pueda alcanzar el estado interno. - «Costuras» (seams) adicionales —un
setClock(...),setRandom(...)oreset()— añadidas solo porque un test las necesitaba, no porque el diseño real las requiera.
// payment-service.ts (código de PRODUCCIÓN)
export class PaymentService {
#ledger: Entry[] = [];
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.
##Reasons for the Problem
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; Meszaros lo señala como la causa principal de For Tests Only.
- 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.
##Treatment
Trata la puerta trasera como una señal de diseño, no como un detalle del fixture.
- 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
getXForTestdesaparecen en cuanto verificas lo que el objeto hace, no lo que contiene. - 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. - 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. - Si la exposición es realmente inevitable, etiquétala a gritos y vállala. Márcala con
@VisibleForTesting/@internal(o una convención de nombresFTO_) 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. - Caza a los infractores existentes. Haz
grepdeforTest/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; }
}
##Detected by
- sonar java:S5803 — Los miembros «@VisibleForTesting» no deben usarse desde el código de producción