Excessive Mocking.
Una prueba monta tantos objetos mock e interacciones simuladas que la configuración de mocks eclipsa la verificación real, de modo que la prueba acaba ejercitando los mocks en lugar del comportamiento real.
##Signs and Symptoms
Una prueba es mayormente preparación: largos bloques de creación de mocks, líneas when(...).thenReturn(...) / mockReturnValue(...) y verify(...), con poco código real bajo prueba. Señales reveladoras:
- Más fontanería de mocks que aserciones. La preparación ocupa muchas líneas; el "actuar" es una sola llamada; el "aseverar" comprueba que los mocks fueron llamados en lugar de que un resultado sea correcto.
- Mockear cosas que son tuyas. Se mockean objetos de dominio, objetos de valor o colaboradores de lógica pura en lugar de solo las fronteras externas (red, BD, reloj, sistema de ficheros, terceros).
- Cadenas de mocks / "trenes descarrilados". Un mock devuelve otro mock que devuelve otro mock, reflejando el grafo de llamadas de producción.
- Verificación solo de interacciones. La prueba asevera
toHaveBeenCalledWith(...)sobre cada colaborador y nunca asevera el valor devuelto ni el estado resultante. - Frágil ante la refactorización. Reordenar llamadas internas o extraer un método rompe muchas pruebas aunque el comportamiento no cambie.
- Cinco o más mocks solo para instanciar el SUT: suele ser señal de que el propio SUT tiene demasiadas dependencias.
test('places order', () => {
const inventory = { check: jest.fn().mockReturnValue(true) };
const pricing = { quote: jest.fn().mockReturnValue(42) };
const tax = { calc: jest.fn().mockReturnValue(4.2) };
const wallet = { charge: jest.fn().mockReturnValue({ ok: true }) };
const ledger = { record: jest.fn() };
const emailer = { send: jest.fn() };
const audit = { log: jest.fn() };
const clock = { now: jest.fn().mockReturnValue(0) };
const svc = new OrderService(inventory, pricing, tax, wallet, ledger, emailer, audit, clock);
svc.place(cart);
// asevera que se llamó a los mocks, no que el pedido sea correcto
expect(inventory.check).toHaveBeenCalled();
expect(pricing.quote).toHaveBeenCalled();
expect(wallet.charge).toHaveBeenCalledWith(46.2);
expect(ledger.record).toHaveBeenCalled();
});
##Reasons for the Problem
Por qué ocurre
- El SUT tiene demasiados colaboradores. El mocking excesivo suele ser una señal de diseño: una clase con poca cohesión y muchas dependencias obliga a cada prueba a levantarlas todas. Como dice la literatura del catálogo, "si necesitas mockear cinco clases internas solo para probar un método, el método tiene demasiadas dependencias: es un problema de diseño, no de pruebas".
- Costumbre / reflejo de "mockearlo todo". Llevar al extremo las pruebas basadas en interacciones ("escuela de Londres"), o mockear por reflejo para evitar tocar una BD o la red, lleva a mockear lógica pura que podría probarse directamente.
- Objetos reales difíciles de construir. Cuando los colaboradores reales son incómodos de instanciar, un mock parece más fácil que arreglar el constructor o añadir un fake.
Por qué es perjudicial
- Falsa confianza. Los mocks codifican tus suposiciones sobre una dependencia. Si la implementación real diverge, la prueba sigue pasando; por ejemplo, un stub que afirma que
sum()devuelve solo enteros positivos mantiene la prueba en verde después de que el método real cambie. Tales pruebas pueden volverse tautológicas: solo verifican que los mocks que escribiste se comportan como los mocks que escribiste. Como advierte la propia documentación de Mockito, "si todo está mockeado, ¿estamos probando de verdad el código de producción?" - Fragilidad / mantenimiento elevado. Verificar llamadas concretas convierte los detalles de implementación en el contrato, así que refactorizaciones inofensivas (orden de llamadas, helpers extraídos) rompen las pruebas. Es el Overspecified Software / Fragile Test de Meszaros.
- Mala legibilidad. Cientos de líneas de configuración de dependencias entierran lo único que de verdad prueba el test, muy parecido a un Mystery Guest: un revisor no puede saber qué comportamiento se está aseverando realmente.
- Errores de integración no detectados. El cableado real entre componentes nunca se ejercita; los errores viven precisamente en las costuras que los mocks reemplazaron. Una cobertura alta enmascara una calidad baja.
##Treatment
Trata el mocking abundante como una señal de retroalimentación y luego reduce la necesidad de mockear:
- Arregla primero el diseño. Si debes mockear más de 5 colaboradores para construir el SUT, divide responsabilidades o reduce las dependencias del constructor. Menos dependencias reales significa menos mocks.
- Mockea solo en las fronteras de la arquitectura. Mockea lo que no es tuyo (red, BD, reloj, sistema de ficheros, APIs de terceros); usa instancias reales de tus propios objetos de dominio y de valor.
- Extrae un núcleo puro (núcleo funcional / cáscara imperativa). Saca la lógica de cálculo/decisión de la clase intensiva en E/S para poder probarla con cero mocks, dejando una cáscara fina que solo necesita uno o dos dobles de frontera.
- Prefiere la verificación de estado a la verificación de interacciones. Asevera el valor devuelto o el estado resultante en lugar de
verify(...)/toHaveBeenCalledWith(...)sobre cada colaborador. Reserva las comprobaciones de interacción para el único efecto secundario que de verdad importa. - Usa el doble más simple que funcione. Sustituye los mocks elaborados por stubs (que solo devuelven valores) o un único fake en memoria reutilizable en vez de volver a stubear cada método en cada prueba.
- Haz aflorar el exceso de mocking de forma dinámica. Activar el strict stubbing de Mockito (el
MockitoExtension/MockitoJUnitRunnerpor defecto) lanzaUnnecessaryStubbingExceptionpara los stubs que se configuran pero nunca se usan: una forma barata de encontrar mocks que no necesitabas.
// ANTES: 8 mocks, aseverando llamadas
const pricing = { quote: jest.fn().mockReturnValue(42) };
const tax = { calc: jest.fn().mockReturnValue(4.2) };
// + 6 mocks más ...
expect(wallet.charge).toHaveBeenCalledWith(46.2);
// DESPUÉS: lógica pura probada directamente, sin mocks
expect(totalFor(cart, rates)).toBe(46.2);
// solo se finge la frontera real; asevera el estado, no las llamadas
const wallet = new InMemoryWallet({ balance: 100 });
const svc = new OrderService(new InMemoryInventory(cart), wallet);
const order = svc.place(cart);
expect(order.total).toBe(46.2);
expect(wallet.balance).toBe(53.8);