Probar detalles de implementación.
Una prueba hace aserciones sobre cómo funciona el código por dentro —campos privados, llamadas a métodos internos, estructura del DOM o clases CSS— en lugar del comportamiento observable del que depende un consumidor real.
##Signs and Symptoms
Reconoces este olor cuando una prueba va más allá del contrato público y fija la maquinaria que hay detrás. Señales habituales:
- Aserciones sobre estado o campos privados/internos, o la invocación directa de métodos privados (a menudo mediante casts, reflexión o
// @ts-expect-error). - Espiar o afirmar que un ayudante interno fue llamado (
expect(internalCalc).toHaveBeenCalled()) en lugar de comprobar el resultado. - Pruebas de UI que consultan por clase CSS, etiqueta, hooks
data-*o posición en el DOM en lugar de por rol/etiqueta/texto; por ejemplo,container.querySelector('.btn-primary > span:nth-child(2)'),wrapper.state(),wrapper.find('SomeChildComponent').props(). - Pruebas de snapshot sobre árboles de renderizado completos / objetos internos serializados, de modo que cualquier retoque del marcado falla.
- Pruebas que se rompen con cada refactorización aunque la funcionalidad siga funcionando (falsos negativos) y, a la inversa, siguen pasando tras un error de lógica porque solo vuelven a comprobar el cableado (falsos positivos).
// SMELL: acopla la prueba a los internos del componente y a la estructura del DOM
test('counter increments', () => {
const wrapper = mount(<Counter />);
wrapper.instance().handleClick(); // llama directamente a un método privado
expect(wrapper.state('count')).toBe(1); // afirma sobre el estado interno
expect(wrapper.find('.count-display').text()).toBe('1'); // selector CSS frágil
});
La misma forma aparece en el lado del servidor: expect(service._cache.size).toBe(1) o afirmar la secuencia exacta de llamadas internas que hace un método.
##Reasons for the Problem
Por qué ocurre
- El estado interno es fácil de alcanzar: un campo público, un ayudante exportado o
container.querySelectorestán ahí mismo, mientras que ejercitar el comportamiento real requiere más preparación. - La persecución de métricas de cobertura: probar cada método privado uno a uno parece minucioso.
- El uso intensivo de mocks empuja a la gente a afirmar «¿se llamó a este colaborador?» en lugar de «¿ocurrió lo correcto?».
- Herramientas que lo fomentan: renderizado superficial (shallow rendering), APIs como
instance()/state(), o la captura de nodos por nombre de clase.
Por qué duele
- Mantenibilidad / fragilidad. Esto es el Test Frágil de Meszaros impulsado por Software Sobreespecificado: la prueba fija un comportamiento que el consumidor nunca requirió, de modo que refactorizaciones inofensivas (renombrar un método, reestructurar el marcado, cambiar un campo privado) rompen pruebas en verde sin motivo real. Las pruebas se convierten en un impuesto sobre la refactorización en lugar de una red de seguridad.
- Falsa confianza (el peligro central, según Kent C. Dodds). Las pruebas de detalles de implementación fallan en ambas direcciones equivocadas: falsos negativos (la prueba se pone en rojo aunque la funcionalidad siga funcionando) y falsos positivos (la prueba sigue en verde aunque la funcionalidad esté rota: afirmaste el cableado, no el resultado). En cualquier caso, la suite deja de decirte la verdad.
- Legibilidad. La prueba documenta cómo está construido el código, no qué garantiza. Quien la lee no puede saber qué comportamiento importa realmente, y la prueba ya no sirve como ejemplo de uso ni como especificación.
- Acoplamiento. Fija las decisiones de diseño actuales, desincentivando precisamente las refactorizaciones que las pruebas deberían hacer seguras.
##Treatment
Prueba a través del contrato público —la misma superficie que toca un llamador o usuario real— y haz aserciones sobre la salida observable: valores de retorno, errores lanzados, eventos emitidos, estado persistido o UI renderizada/visible.
- Identifica al consumidor. Para un módulo, es su API exportada; para un componente de UI, es el usuario (clics, escritura) y lo que puede ver.
- Proporciona las entradas como lo haría un consumidor, no llamando a métodos privados. Dispara un clic real en lugar de invocar el manejador; llama al método público en lugar al ayudante.
- Haz aserciones sobre resultados, no sobre internos. Sustituye las comprobaciones de
state()/campo-privado/toHaveBeenCalledpor comprobaciones de lo que se produce. - Consulta la UI por accesibilidad, no por estructura —rol, etiqueta, texto— en lugar de clases CSS, etiquetas o
nth-child. - Deja de probar métodos privados directamente. Cúbrelos a través del método público que los usa; si una unidad privada es lo bastante compleja como para necesitar sus propias pruebas, es señal de que conviene extraerla a su propio módulo con su propia API pública.
- Reserva las aserciones sobre mocks/espías para fronteras reales (red, tiempo, pasarela de pago), donde la llamada en sí es el comportamiento observable, no para colaboradores internos.
// ANTES: prueba detalles de implementación
const wrapper = mount(<Counter />);
wrapper.instance().handleClick();
expect(wrapper.state('count')).toBe(1);
expect(wrapper.find('.count-display').text()).toBe('1');
// DESPUÉS: prueba el comportamiento observable a través del contrato público de cara al usuario
render(<Counter />);
await userEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('1')).toBeInTheDocument();
Regla práctica: si una refactorización que preserva el comportamiento rompe la prueba, la prueba estaba afirmando un detalle de implementación.
##Detected by
- eslint-testing-library testing-library/no-node-access — no-node-access
- eslint-testing-library testing-library/no-container — no-container