Mocking HTTP con WireMock

Avanzado
Spring Boot
Spring Boot
Actualizado: 07/05/2026

Diagrama: tutorial-spring-boot-wiremock-mock-http

El problema de testear integraciones

Tu microservicio llama a una API externa: el ERP de un cliente, Stripe, un OAuth2 de Azure, un endpoint REST de un partner. Los tests "felices" pasan en local porque la API responde, pero en CI fallan porque no hay red, en producción fallan porque la API devuelve un 503 y nadie escribió un test para ese caso, y en @SpringBootTest cada ejecución tarda 12 segundos esperando respuestas reales.

Mockear con @MockBean el cliente Java funciona para tests unitarios, pero deja sin verificar el verdadero acoplamiento: cómo se construye la petición HTTP, qué headers se envían, cómo se parsea la respuesta JSON, cómo se manejan los códigos de estado. Lo que se necesita es un servidor HTTP local que se comporte como la API real, pero que esté bajo control del test.

Esa es exactamente la propuesta de WireMock.

WireMock en 30 segundos

WireMock arranca un servidor HTTP (Jetty) en un puerto local y permite registrar stubs: pares de "si llega esta petición, responde esto". El test apunta el cliente a http://localhost:{wiremockPort}/api, ejecuta el flujo y verifica que las peticiones esperadas se realizaron con los headers y el body correctos.

graph LR
    A[Test JUnit 6] --> B[Servicio bajo test]
    B --> C[RestClient HTTP]
    C --> D[WireMock localhost:8089]
    D --> E[Respuesta stub configurada]
    A --> F[Verify peticiones]

Instalación

Para Spring Boot 4 y JUnit 6, WireMock se añade así:

<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock-standalone</artifactId>
    <version>3.10.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-wiremock</artifactId>
    <version>4.2.0</version>
    <scope>test</scope>
</dependency>

El módulo spring-cloud-contract-wiremock aporta la anotación @AutoConfigureWireMock, que arranca el servidor automáticamente y configura propiedades del entorno Spring para que el cliente apunte a la URL de WireMock.

Primer stub básico

Un test típico contra una API externa de pagos:

@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
class PagoServiceIT {

    @Autowired
    private PagoService service;

    @Value("${wiremock.server.port}")
    private int wiremockPort;

    @Test
    void crea_pago_y_devuelve_id() {
        stubFor(post(urlEqualTo("/v1/payments"))
            .willReturn(okJson("""
                {
                  "id": "pago_123",
                  "estado": "OK"
                }
                """)
            )
        );

        var resultado = service.crear(new PagoDto("EUR", 1500));

        assertThat(resultado.id()).isEqualTo("pago_123");
    }
}

@AutoConfigureWireMock(port = 0) levanta el servidor en un puerto aleatorio. La propiedad wiremock.server.port queda inyectada en el Environment, y la base URL del cliente HTTP debe leer de ${wiremock.server.baseUrl} o componer http://localhost:${wiremock.server.port}.

Request matching avanzado

Los stubs no son útiles si responden a cualquier petición. WireMock permite matchear por método, URL, headers, query params y body con bastante precisión:

stubFor(post(urlPathEqualTo("/v1/customers"))
    .withHeader("Authorization", matching("Bearer .+"))
    .withHeader("Content-Type", equalTo("application/json"))
    .withQueryParam("expand", equalTo("subscriptions"))
    .withRequestBody(matchingJsonPath("$.email", containing("@")))
    .withRequestBody(matchingJsonPath("$.name"))
    .willReturn(okJson("""
        { "id": "cust_99", "email": "ana@empresa.com" }
        """)
        .withStatus(201)
    )
);

Algunos matchers útiles:

  • urlPathEqualTo, urlPathMatching, urlPathTemplate("/users/{id}") para paths.
  • equalToJson(json, true, false) para body JSON con tolerancia a orden y campos extra.
  • matchingJsonPath("$.items[0].sku", equalTo("ABC")) para verificar campos concretos.
  • equalToXml, matchingXPath para SOAP o respuestas XML.

Cuando ningún stub matchea, WireMock devuelve 404 y registra la petición en el log. Es la forma más rápida de detectar que el servicio bajo test envía algo distinto a lo esperado.

Verificación de peticiones realizadas

Stubear la respuesta es la mitad del test. La otra mitad es verificar el contrato saliente: comprobar que tu servicio envía los headers, el path y el body correctos. WireMock guarda un journal de peticiones recibidas:

verify(exactly(1), postRequestedFor(urlEqualTo("/v1/customers"))
    .withHeader("Authorization", equalTo("Bearer token-test"))
    .withHeader("Idempotency-Key", matching("[a-f0-9-]{36}"))
    .withRequestBody(matchingJsonPath("$.email", equalTo("ana@empresa.com")))
);

verify(0, deleteRequestedFor(anyUrl()));

Esto detecta regresiones sutiles: alguien añade un campo nuevo al DTO y olvida mapearlo al body JSON, alguien renombra un header, alguien deja de enviar la idempotency key. Sin verificación, el test pasaría aunque el contrato HTTP cambiase silenciosamente.

Fault injection: simular fallos reales

Una API externa puede fallar de muchas formas. WireMock permite inyectar fallos para probar la resilencia del cliente:

stubFor(get(urlEqualTo("/v1/payments/123"))
    .willReturn(aResponse()
        .withStatus(503)
        .withFixedDelay(2000)
        .withBody("Service Unavailable")
    )
);

stubFor(get(urlEqualTo("/v1/slow"))
    .willReturn(aResponse()
        .withFixedDelay(10000)
    )
);

stubFor(get(urlEqualTo("/v1/connection-reset"))
    .willReturn(aResponse()
        .withFault(Fault.CONNECTION_RESET_BY_PEER)
    )
);

stubFor(get(urlEqualTo("/v1/garbage"))
    .willReturn(aResponse()
        .withFault(Fault.MALFORMED_RESPONSE_CHUNK)
    )
);

Estos escenarios son la forma idiomática de validar que tu cliente:

  • Aplica timeouts y no se queda colgado indefinidamente.
  • Hace retries con backoff sobre 5xx pero no sobre 4xx.
  • Devuelve un error de dominio claro ante respuestas malformadas, no un NullPointerException.

Stateful behavior con scenarios

Algunos flujos requieren que la respuesta cambie según el estado: un OAuth2 cuyo token expira tras la primera llamada, un endpoint que devuelve 202 Accepted y luego 200 OK cuando el job termina. WireMock modela esto con scenarios:

private static final String SCENARIO = "OAuth Token";

stubFor(post(urlEqualTo("/oauth/token"))
    .inScenario(SCENARIO)
    .whenScenarioStateIs(STARTED)
    .willReturn(okJson("""
        { "access_token": "tok-1", "expires_in": 3600 }
        """))
    .willSetStateTo("primer-token-emitido")
);

stubFor(post(urlEqualTo("/oauth/token"))
    .inScenario(SCENARIO)
    .whenScenarioStateIs("primer-token-emitido")
    .willReturn(okJson("""
        { "access_token": "tok-2", "expires_in": 3600 }
        """))
);

Esto permite verificar que el cliente refresca el token tras un 401, que mantiene la cuenta correcta de llamadas, o que reintenta exactamente N veces antes de propagar el error.

Record-and-replay

Para integraciones nuevas o APIs poco documentadas, WireMock puede capturar las peticiones y respuestas reales. Se arranca apuntando a la API real y guarda los stubs en disco:

@Test
void captura_respuesta_real_para_replay() {
    var server = new WireMockServer(options().port(8089).usingFilesUnderDirectory("src/test/resources/wiremock"));
    server.start();
    server.startRecording("https://api.partner.com");

    // Ejecutar el flujo del cliente apuntado a localhost:8089
    cliente.consultarFactura("F-001");

    server.stopRecording();
    server.stop();
}

Los stubs quedan en src/test/resources/wiremock/mappings/. En adelante, los tests cargan esos ficheros con WireMockServer.usingFilesUnderDirectory(...) y ejecutan offline. Es el flujo más rápido para tipificar contratos de APIs de terceros, especialmente cuando la documentación oficial es incompleta.

Integración con RestClient en Spring Boot 4

RestClient es la API HTTP recomendada en Spring Framework 7. La configuración del bean apunta a la URL de WireMock leyendo del Environment:

@Configuration
public class HttpClientConfig {

    @Bean
    public RestClient pagosRestClient(@Value("${pagos.api.base-url}") String baseUrl,
                                      @Value("${pagos.api.token}") String token) {
        return RestClient.builder()
            .baseUrl(baseUrl)
            .defaultHeader("Authorization", "Bearer " + token)
            .defaultHeader("Content-Type", "application/json")
            .build();
    }
}

En application-test.yaml:

pagos:
  api:
    base-url: ${wiremock.server.baseUrl}
    token: token-test

Con esto, el RestClient apunta a WireMock durante los tests sin cambios en el código de producción.

Ejemplo: stub OAuth2 para Spring Security

Un escenario habitual: un servicio que valida JWTs emitidos por un Authorization Server externo. Levantar Keycloak en CI es lento; con WireMock se mockean los endpoints /.well-known/openid-configuration y /jwks:

@BeforeEach
void stubOAuthDiscovery() {
    stubFor(get(urlEqualTo("/.well-known/openid-configuration"))
        .willReturn(okJson("""
            {
              "issuer": "http://localhost:%d",
              "jwks_uri": "http://localhost:%d/jwks"
            }
            """.formatted(wiremockPort, wiremockPort))
        )
    );

    stubFor(get(urlEqualTo("/jwks"))
        .willReturn(okJson(jwksJson))
    );
}

Con eso, Spring Security valida los tokens JWT firmados localmente con la clave conocida, sin necesidad de Keycloak ni acceso a Internet. El test queda determinista y rápido.

Buenas prácticas

  • Un fichero de stubs por escenario: usar usingFilesUnderDirectory y guardar los mappings en src/test/resources/wiremock/. Mantiene los tests legibles cuando hay 20-30 stubs.
  • Resetear el journal entre tests con WireMock.reset() o @AutoConfigureWireMock, que ya lo hace por defecto. Si no, las verificaciones acumulan llamadas de tests anteriores.
  • Verificar siempre el contrato saliente: stubear la respuesta no basta. Sin verify(...) el test pasaría aunque el cliente enviase un body vacío.
  • Inyectar al menos un test de fallo (timeout o 503) por integración crítica: garantiza que el manejo de errores se ejercita y no es código muerto.
  • No confundir WireMock con tests E2E: WireMock simula la API. Un E2E real (con Testcontainers y la API real cuando es posible) sigue siendo necesario para detectar incompatibilidades de versión.
  • Usar record-and-replay para tipificar APIs nuevas, pero revisar y limpiar los stubs antes de commitearlos: una grabación cruda incluye headers innecesarios y datos sensibles.
  • Coordinar con Spring Cloud Contract cuando ambos lados (consumer y provider) son tuyos: WireMock se queda corto comparado con un contrato compartido y verificado en ambos extremos.

WireMock convierte tests de integración frágiles en tests deterministas que se ejecutan en milisegundos. La inversión inicial (escribir los stubs, organizar los ficheros, aprender los matchers) se amortiza en la primera incidencia detectada antes de producción y en la primera CI que deja de fallar por culpa de una API externa caída.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Boot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Spring Boot

Explora más contenido relacionado con Spring Boot y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Levantar WireMock embebido en tests JUnit 6 con @AutoConfigureWireMock o WireMockExtension. Definir stubs con request matching avanzado (jsonPath, regex, headers). Verificar el contrato de salida con verify(getRequestedFor(...)). Simular fallos transitorios (timeouts, 503) para validar resilencia. Usar scenarios para flujos stateful tipo OAuth2. Capturar respuestas reales con record-and-replay y reproducirlas en CI.