Escribir Unit Tests con Mocks y Stubs
Cómo aislar código bajo test usando objetos mock, stubs y spies para reemplazar dependencias externas como bases de datos, APIs y sistemas de archivos.
Visión General
Un unit test debería verificar una sola función o clase sin nada más en el medio. El problema es que la mayoría del código depende de cosas que no querés correr en un suite de tests — bases de datos, APIs HTTP, sistema de archivos, el reloj, generadores de números aleatorios. El mocking te permite cambiar esas dependencias por sustitutos controlados que devuelven el valor que necesitás, lanzan una excepción específica, o registran cómo fueron llamados.
Una vez pasé dos días debuggeando un test flaky que estaba mockeando la capa equivocada. El test reemplazaba un método helper interno en lugar del cliente de base de datos, así que cada refactor rompía el test aunque el código funcionara bien. Eso me enseñó la regla de oro del mocking: siempre mockeá en el boundary, nunca dentro de la clase bajo test.
Esta receta cubre los tres test doubles que más vas a usar — stubs, mocks y spies — con ejemplos en JavaScript (Jest 29.x), Python (Pytest 8.x) y Java (Mockito 5.x).
Cuándo Usarlo
Los mocks y stubs son una buena opción cuando tus unit tests tocan bases de datos, APIs o servicios de terceros. También sirven cuando querés simular un error difícil de disparar en un ambiente real, acelerar un suite lento, verificar que una función llamó a un colaborador con los argumentos correctos, o reemplazar dependencias no determinísticas como la hora actual, UUIDs o valores random. Para Snapshot Testing, los mocks son esenciales para aislar el componente de sus dependencias.
Cuándo NO Usarlo
Si una dependencia es rápida, determinística y simple, usá la real en lugar de un mock. Cuando necesitás validar cómo interactúan varios componentes reales, mirá Integration Testing. Y cuando el objetivo es confirmar el contrato HTTP real de un servicio externo, herramientas como WireMock o API Mocking suelen ser más apropiadas.
Solución
Mock con Jest (JavaScript)
import { processPayment } from './payment';
import { sendEmail } from './email';
jest.mock('./email');
test('sends receipt email after successful payment', async () => {
sendEmail.mockResolvedValue({ messageId: '123' });
await processPayment({ amount: 100, userId: 'u1' });
expect(sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
to: 'user@example.com',
subject: 'Payment received',
})
);
});
test('handles email service failure gracefully', async () => {
sendEmail.mockRejectedValue(new Error('SMTP down'));
const result = await processPayment({ amount: 100, userId: 'u1' });
expect(result.emailSent).toBe(false);
expect(result.paymentId).toBeDefined();
});
Mock con Pytest (Python)
from unittest.mock import patch, MagicMock
from payment import process_payment
def test_payment_success():
with patch('payment.send_email') as mock_email:
mock_email.return_value = {'message_id': '123'}
result = process_payment(amount=100, user_id='u1')
assert result['email_sent'] is True
mock_email.assert_called_once()
def test_payment_email_failure():
with patch('payment.send_email', side_effect=SMTPError('timeout')):
result = process_payment(amount=100, user_id='u1')
assert result['email_sent'] is False
Stub con Mockito (Java)
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
class PaymentServiceTest {
@Test
void sendsReceiptOnSuccess() {
EmailService emailMock = mock(EmailService.class);
when(emailMock.send(any())).thenReturn(new Receipt("123"));
PaymentService service = new PaymentService(emailMock);
service.processPayment(100, "u1");
verify(emailMock, times(1)).send(argThat(receipt ->
receipt.getSubject().equals("Payment received")
));
}
}
Explicación
Un stub devuelve una respuesta prefabricada. Podés construir un stub de base de datos que entregue un registro de usuario hardcodeado. El código bajo test obtiene los datos que necesita, y al stub no le importa si alguien lo llamó.
Un mock es más estricto: viene programado con expectativas y falla el test si no se lo llama la cantidad correcta de veces o con los argumentos correctos. Usá un mock cuando la interacción misma es parte del contrato.
Un spy envuelve un objeto real y registra sus llamadas para verificarlas después. Por ejemplo, podés espiar una caché para asegurarte de que fue consultada antes de que el código golpee la base de datos.
La regla principal es mockear en el boundary. Reemplazá el cliente HTTP o el driver de base de datos, no métodos privados dentro de la clase que estás testeando. Mockear de más hace los tests frágiles y le quita sentido al unit testing.
Algo que me trancó al principio: Jest 29.x cambió cómo jest.mock() hoistea los auto-mocks. Si
referenciás una variable en la función factory, tenés que prefijarla con mock para evitar el
error “Out-of-order variable access”. Pytest 8.x tiene un gotcha similar con patch() — el target
del patch debe ser el path fully qualified donde se busca el nombre, no donde se define. Y Mockito
5.x dejó de soportar inline mocks por defecto; ahora necesitás la dependencia mockito-inline
por separado.
Variantes
| Double | Reemplaza | Verifica llamadas | Mejor para |
|---|---|---|---|
| Dummy | Parámetro no usado | No | Llenar listas de argumentos |
| Fake | Implementación funcional | No | Base de datos en memoria |
| Stub | Respuesta específica | No | Devolver datos de test |
| Spy | Objeto real + registra | Sí | Verificar side effects |
| Mock | Interacción esperada | Sí | Verificar llamadas hechas |
Usá un stub cuando solo necesitás alimentar datos al código bajo test. Usá un mock cuando la interacción misma importa. Usá un spy cuando querés que corra la implementación real y solo querés verificar que fue usada.
Buenas Prácticas
- Mockeá en el boundary, no dentro de la clase bajo test. Reemplazá el cliente HTTP o el driver de base de datos, no cada método privado.
- Preferí stubs para verificación de estado cuando podés. Assertar sobre el estado final (“el balance es $50”) suele ser más resistente al refactoring que assertar sobre la interacción (“withdraw fue llamado una vez”).
- Reseteá el estado de los mocks entre tests. Jest y Pytest hacen esto automáticamente; en otros frameworks, creá instancias frescas para cada test.
- Confiá en la inyección de dependencias. El código que instancia sus propias dependencias con
new Database()es difícil de mockear. Inyectalas vía constructores o factories. - No mockees objetos de valor. Las clases simples de datos, structs y DTOs no tienen comportamiento real, así que pasá instancias reales.
- Mantené las expectativas de mock acotadas. Verificá solo las llamadas que importan, porque especificar demasiado ata los tests a detalles de implementación.
Errores Comunes
- Mockear el sistema bajo test en lugar de sus colaboradores. Cuando mockeás métodos dentro de la clase que estás testeando, ya no estás testeando esa clase.
- Especificar interacciones en exceso, como afirmar que
database.connect()fue llamado exactamente una vez. Eso ata el test a detalles de implementación. - Configurar
verify()pero nunca llamarlo — el test parece completo, pero te estás engañando a vos mismo. - Mockear cada clase en un test. Si todo está mockeado, el suite termina testeando los mocks, no el sistema real.
- Dejar que los mocks se separen del contrato real. Un mock HTTP que devuelve un shape distinto al de la API productiva puede ocultar bugs reales.
Resumen
- Mockeá en el boundary — reemplazá el cliente HTTP o el driver de base de datos, no métodos privados dentro de la clase bajo test. Mockear de más hace los tests frágiles.
- Preferí stubs para verificación de estado — assertar sobre el estado final (“el balance es $50”) es más resistente que assertar sobre interacciones (“withdraw fue llamado una vez”).
- Usá el double correcto para cada caso — stub para datos prefijados, mock para verificación de interacción, spy para impl-real-plus-registro, fake para implementaciones en memoria.
- Mantené las expectativas de mock acotadas — verificá solo las llamadas que importan. Especificar demasiado ata los tests a detalles de implementación.
- Inyectá dependencias — el código que hace
new Database()internamente es una pesadilla de mockear. Pasá las dependencias por constructores o factories.
Código complementario: El repo companion de unit-testing-mocking tiene ejemplos ejecutables de Jest, Pytest y Mockito para cada patrón mostrado acá.
See Also
- Documentación de Jest Mock Functions
— guía oficial de
jest.mock(),mockReturnValueymockResolvedValueen Jest 29.x. - Documentación de Pytest Monkeypatch y Mock
— guía oficial de
monkeypatchyunittest.mock.patchen Pytest 8.x. - Documentación de Mockito
— referencia oficial de
mock(),when(),verify()y argument matchers en Mockito 5.x. - Martin Fowler — TestDouble — definición canónica de test doubles (dummy, stub, spy, fake, mock) por Martin Fowler.
- xUnit Patterns — Test Double Patterns — catálogo completo de patrones de test doubles con ejemplos detallados.
Preguntas frecuentes
¿Cuándo debería usar una dependencia real en lugar de un mock?
Usá la implementación real cuando sea rápida, determinística y simple — por ejemplo, un Map en memoria o una función pura. Mientras más cerca esté tu test de producción, más confianza útil te va a dar.
¿Cuál es la diferencia entre un stub y un mock?
Un stub responde llamadas con datos prefijados. Un mock verifica que se hicieron las llamadas esperadas. Un mock puede actuar como stub, pero un stub no puede actuar como mock.
¿Debería mockear el sistema de archivos?
Para tests unitarios, sí — usá sistemas de archivos virtuales o streams en memoria. Para tests de integración, escribí a un directorio temporal y limpialo después.
¿Puedo mockear métodos estáticos?
En Java, PowerMock y Mockito inline mock pueden hacerlo, pero generalmente no deberías. Los métodos estáticos son difíciles de testear porque no pueden inyectarse. Refactorizá a métodos de instancia cuando sea posible.
¿Cómo evito el over-mocking?
Mockeá solo las dependencias externas que sean lentas, no determinísticas o no disponibles en tests. Si un colaborador es rápido y determinístico, usá el real. Cuando dudes, empezá con un stub.
¿Cuándo uso un spy?
Usá un spy cuando querés que el objeto real corra pero también necesitás verificar cómo fue llamado. Ejemplos comunes incluyen chequear que un logger escribió una advertencia o que una caché fue consultada antes de una query lenta.
Recursos Relacionados
Pruebas Unitarias
Cómo escribir pruebas unitarias rápidas y deterministas con mocks y assertions en Python, JavaScript y Java.
RecipeEscribir Tests de Integración
Cómo testear múltiples componentes trabajando juntos usando bases de datos reales, clientes HTTP y colas de mensajes en Python, JavaScript y Java.
RecipeAPI Mocking para Testing
Construye tests confiables mockeando APIs externas con WireMock, MockServer y MSW para eliminar flakiness y testear casos edge.
RecipeSnapshot Testing de Componentes React con Jest
Como usar snapshot testing de Jest para detectar regresiones de UI no intencionales en componentes React y prevenir bugs visuales en produccion
RecipeMockear APIs Externas con responses
Cómo mockear llamadas HTTP en tests de Python usando la librería responses, incluyendo códigos de estado, headers, bodies JSON y simulación de errores.
RecipeStubear APIs HTTP Externos con WireMock en Java
Usá WireMock en tests de Java para stubear servicios HTTP externos. Cubre templating, simulación de delays, stubs stateful y verificación de requests.