StackPractices
intermediate Por Mathias Paulenko

API Mocking para Testing

Construye tests confiables simulando APIs externas con WireMock, MockServer y MSW para eliminar inestabilidad y probar casos límite.

Visión General

El API mocking sustituye dependencias externas reales por simulaciones controladas durante el testing. Tu suite deja de depender de la red: cero inestabilidad, ejecuciones más rápidas y casos límite como errores 500 o timeouts que se vuelven triviales de reproducir. Cada herramienta simula la frontera de forma distinta — WireMock corre como un servidor HTTP real, MSW intercepta requests in-process y responses parchea la capa HTTP de Python — así que eliges la que mejor encaje en tu stack.

Cuándo Usar

Usa este recurso cuando:

  • Las APIs externas son poco confiables, lentas o tienen rate limits que bloquean pipelines de CI
  • Necesitas probar el manejo de errores para HTTP 429, 503 o escenarios de timeout
  • El servicio real no tiene un sandbox o ambiente de pruebas
  • Quieres tests determinísticos que no fallen por cambios de terceros

Evita el API mocking cuando:

  • Pruebas tu propia base de datos o lógica interna — usa un motor en memoria o Testcontainers y ejecuta lo real
  • Necesitas pruebas de que el proveedor cumple su API — para eso está el contract testing

Solución

WireMock Standalone (Java)

import com.github.tomakehurst.wiremock.WireMockServer;
import static com.github.tomakehurst.wiremock.client.WireMock.*;

public class PaymentApiMock {
    private static WireMockServer wireMockServer;

    public static void start() {
        wireMockServer = new WireMockServer(8089);
        wireMockServer.start();

        wireMockServer.stubFor(
            post(urlEqualTo("/payments"))
                .withHeader("Content-Type", equalTo("application/json"))
                .withRequestBody(matchingJsonPath("$.amount"))
                .willReturn(aResponse()
                    .withStatus(200)
                    .withHeader("Content-Type", "application/json")
                    .withBody("{\"id\": \"pay_123\", \"status\": \"succeeded\"}")
                )
        );

        // Escenario de error
        wireMockServer.stubFor(
            post(urlEqualTo("/payments"))
                .withRequestBody(matchingJsonPath("$.amount", equalTo("999999")))
                .willReturn(aResponse()
                    .withStatus(400)
                    .withBody("{\"error\": \"amount_exceeds_limit\"}")
                )
        );
    }

    public static void stop() {
        wireMockServer.stop();
    }
}

MSW (Mock Service Worker) para Browser/Node

// MSW 2.x — la API `rest` de la v1 fue eliminada
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

const handlers = [
  http.get('https://api.example.com/users/:id', ({ params }) => {
    const { id } = params;
    return HttpResponse.json({ id, name: 'Test User', email: 'test@example.com' });
  }),

  http.post('https://api.example.com/orders', async ({ request }) => {
    const { total } = await request.json();
    return HttpResponse.json({ orderId: 'ord_456', total }, { status: 201 });
  }),

  // Simulación de error
  http.get('https://api.example.com/flaky', () => {
    return HttpResponse.json({ error: 'Service Unavailable' }, { status: 503 });
  })
];

const server = setupServer(...handlers);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Python responses Library

import responses
import requests

@responses.activate
def test_payment_api():
    responses.add(
        responses.POST,
        'https://payments.example.com/charge',
        json={'id': 'ch_123', 'status': 'succeeded'},
        status=200
    )

    result = requests.post(
        'https://payments.example.com/charge',
        json={'amount': 100, 'currency': 'USD'}
    )

    assert result.json()['status'] == 'succeeded'
    assert len(responses.calls) == 1
    assert responses.calls[0].request.json()['amount'] == 100

Simulación de Latencia y Fallos

Los mocks rápidos que siempre devuelven 200 esconden los bugs que importan. Hazlos lentos e inestables a propósito:

// MSW 2.x: latencia y fallos a nivel de red
import { http, HttpResponse, delay } from 'msw';

http.get('https://api.example.com/slow-report', async () => {
  await delay(5000); // dispara cualquier timeout de cliente menor a 5s
  return HttpResponse.json({ ok: true });
}),

http.get('https://api.example.com/unreachable', () => {
  return HttpResponse.error(); // fallo de red real, no un status HTTP
})

HttpResponse.error() dispara un fallo a nivel de conexión — un reset o un error de DNS, algo que un 503 jamás reproduce. Añade llamadas a delay() para comprobar que el cliente respeta su propio presupuesto de timeout; una request que nunca resuelve duele más que un 500.

Mocks con Estado

Las APIs reales cambian de estado entre llamadas — un job pasa de pending → running → done. WireMock lo modela con escenarios, donde un stub puede transicionar el escenario tras coincidir:

// El primer poll devuelve 202, el siguiente 200 — prueba el polling sin sleeps reales
wireMockServer.stubFor(get(urlEqualTo("/jobs/42"))
    .inScenario("job-lifecycle")
    .whenScenarioStateIs(Scenario.STARTED)
    .willReturn(aResponse().withStatus(202).withBody("{\"state\": \"running\"}"))
    .willSetStateTo("done"));

wireMockServer.stubFor(get(urlEqualTo("/jobs/42"))
    .inScenario("job-lifecycle")
    .whenScenarioStateIs("done")
    .willReturn(aResponse().withStatus(200).withBody("{\"state\": \"done\"}")));

Los escenarios permiten que un endpoint responda distinto en secuencia — la única forma sensata de probar polling, reintentos con recuperación y workflows de varios pasos.

Explicación

Tres estrategias de mocking:

EstrategiaNivelIdeal Para
HTTP server proxyRedTests de integración; verificar clientes HTTP reales
Request interceptorAplicaciónTests unitarios; mocking unificado browser/Node
Service virtualizationSistemaAPIs stateful complejas; contract testing

Jerarquía de request matching:

  1. URL exacta — GET /users/123
  2. Patrón de path — GET /users/*
  3. Coincidencia de header — Content-Type: application/json
  4. Coincidencia de body — JSON path o regex sobre el cuerpo de la request
  5. Dependiente de estado — Devolver una respuesta diferente en la segunda llamada

Mock vs. stub vs. fake vs. contract test — se confunden constantemente:

Técnica¿Ejecuta código real?¿Registra llamadas?¿Verifica al proveedor?
StubNoNoNo
MockNoSíNo
FakeSí, simplificadoNoNo
Contract testCódigo real del proveedor—Sí

Elige según la pregunta que respondes: «¿mi código maneja la respuesta X?» → stub o mock. «¿Mi código llama bien a la API?» → mock. «¿Mis queries funcionan?» → fake o una base de datos real. «¿El proveedor sigue cumpliendo su contrato?» → contract test.

Mermaid flowchart LR diagram

Variantes

HerramientaLenguajeMejor Feature
WireMockJava/CualquieraEscenarios stateful; proxy recording
MSWTypeScriptMismos mocks en browser, Node y tests
MockServerCualquieraAPI de expectativas JSON; verificación
responsesPythonBasado en decoradores; assertions simples
NockNode.jsAPI encadenada; modo recorder

Lo que funciona

  • Simula en la frontera: Mockea HTTP, no métodos internos — los tests deberían ejercitar el stack completo.
  • Verifica requests, no solo responses: Asegúrate de que tu código envía el payload y headers correctos
  • Usa record/replay para APIs complejas: Captura tráfico real una vez, luego replay en tests
  • Mantén mocks cerca de la realidad: Actualiza mocks cuando la API real cambia; mocks obsoletos ocultan bugs
  • Limpia estado entre tests: Un reset evita que el setup de un test afecte a otro

Errores Comunes

  1. Mockear métodos internos: Pruebas el mock, no el código
  2. Matchers demasiado permisivos: Matchers any() dejan pasar bugs que matchers específicos detectan
  3. Sin cobertura de escenarios de error: Probar solo 200 OK omite la mitad del código de manejo de errores
  4. Estado mutable compartido: El estado global de los mocks se filtra entre tests
  5. Olvidar verificar: Un test que pasa con un mock sin usar significa que nada quedó realmente probado

Lectura Adicional

Preguntas frecuentes

¿Debería simular la base de datos de mi propio servicio?

No. Usa mocks en las fronteras que no controlas — una API de terceros, una pasarela de pago, un proveedor de email. Tu base de datos es otra historia: levántala de verdad con Testcontainers o un motor en memoria. Una DB simulada te dice que el mock funciona; no dice nada de tus queries.

¿Cuál es la diferencia entre mocking y stubbing?

Los stubs devuelven respuestas prefijadas y olvidan que la llamada ocurrió. Los mocks registran cada llamada para que puedas verificarla — «¿mi código llamó a este endpoint con estos args?». Usa stubs para datos, y mocks cuando la interacción misma es lo que estás probando.

¿Los mocks pueden reemplazar al contract testing?

No. Un mock responde «si la API devuelve X, ¿mi código hace Y?» — nunca pregunta si el proveedor realmente devuelve X. Esa segunda pregunta es para lo que existen los contract tests. Quieres ambos: los mocks mantienen la suite rápida, los contratos la mantienen honesta.

¿Cómo mantengo los mocks sincronizados con la API real?

Derívalos de la spec OpenAPI del proveedor o de tráfico grabado — record-and-play de WireMock, cassettes tipo VCR — y regraba periódicamente. Un mock viejo sigue pasando tests contra una API que ya no existe; ese drift es el verdadero precio de simular.

¿Los mock servers deberían vivir en mi suite de tests o en un entorno compartido?

En la suite, levantados en cada test — los mock servers compartidos se convierten en estado mutable compartido y CI inestable. WireMock, MSW y responses corren in-process o por test; reserva un entorno compartido solo para QA exploratorio.