StackPractices
beginner Por Mathias Paulenko

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.

Temas: testing

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

flowchart diagram: ¿Necesitás un test double?

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

DoubleReemplazaVerifica llamadasMejor para
DummyParámetro no usadoNoLlenar listas de argumentos
FakeImplementación funcionalNoBase de datos en memoria
StubRespuesta específicaNoDevolver datos de test
SpyObjeto real + registraVerificar side effects
MockInteracción esperadaVerificar 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

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.