Skip to content
StackPractices
intermediate Por Mathias Paulenko

Stubear Servicios HTTP Externos con WireMock

Cómo usar WireMock en tests de Java para stubear servicios HTTP externos, incluyendo templating de respuestas, simulación de delays y comportamiento stateful.

Temas: testing

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Overview

WireMock es una librería de Java que arranca un servidor HTTP y te permite definir stub mappings para servicios externos. En lugar de llamar APIs reales de terceros, tu código hittea WireMock, que devuelve respuestas predefinidas. Controlas códigos de estado, headers, bodies, delays e incluso comportamiento stateful — todo desde tu código de test.

When to Use

  • Testear código que llama APIs REST externas (payment gateways, SMS, email providers)
  • Simular fallos de API (timeouts, 500s, respuestas lentas) para testear lógica de retry
  • Verificar que tu código envía la petición correcta a un servicio externo
  • Testear webhook receivers sin un sender real
  • Correr tests de integración en CI sin acceso a red

When NOT to Use

  • Testear tus propios endpoints de API — usa MockMvc o WebTestClient para Spring
  • Unit testing de lógica de negocio — mockea la interfaz directamente con Mockito
  • Load testing — WireMock añade overhead; usa un entorno de test real en su lugar
  • Testear interacciones con base de datos — usa Testcontainers con una DB real

Solution

Setup con JUnit5

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8</artifactId>
    <version>3.9.1</version>
    <scope>test</scope>
</dependency>

Stub básico con extensión de JUnit5

import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.junit.jupiter.api.Assertions.assertEquals;

class ExternalServiceTest {

    @RegisterExtension
    static WireMockExtension wireMock = WireMockExtension.newInstance()
        .options(wireMockConfig().dynamicPort())
        .build();

    @Test
    void shouldStubGetRequest() throws Exception {
        wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/users/1"))
            .willReturn(WireMock.aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("{\"id\":1,\"name\":\"Alice\"}")));

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(HttpRequest.newBuilder()
                .uri(URI.create(wireMock.getRuntimeInfo().getHttpBaseUrl() + "/api/users/1"))
                .GET().build(), HttpResponse.BodyHandlers.ofString());

        assertEquals(200, response.statusCode());
        assertEquals("Alice", parseJson(response.body(), "name"));
    }
}

Stub con JSON body desde archivo

wireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo("/api/products"))
    .willReturn(WireMock.aResponse()
        .withStatus(200)
        .withBodyFile("products-response.json")));

Coloca products-response.json en src/test/resources/__files/.

Simular delay y timeout

wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/slow"))
    .willReturn(WireMock.aResponse()
        .withStatus(200)
        .withFixedDelay(5000)
        .withBody("{\"data\":\"delayed\"}")));

Simular respuestas de error

wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/error"))
    .willReturn(WireMock.aResponse()
        .withStatus(500)
        .withBody("{\"error\":\"Internal Server Error\"}")));

wireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/api/rate-limited"))
    .willReturn(WireMock.aResponse()
        .withStatus(429)
        .withHeader("Retry-After", "60")
        .withBody("{\"error\":\"Rate limit exceeded\"}")));

Verificar que se hicieron las peticiones

wireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/api/orders"))
    .withRequestBody(WireMock.matchingJsonPath("$.product_id", WireMock.equalTo("10")))
    .withHeader("Authorization", WireMock.matching("Bearer .*")));

Stubbing stateful con escenarios

wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/order/status"))
    .inScenario("Order Processing")
    .whenScenarioStateIs("Started")
    .willReturn(WireMock.aResponse().withBody("{\"status\":\"pending\"}"))
    .willSetStateTo("Processing"));

wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/order/status"))
    .inScenario("Order Processing")
    .whenScenarioStateIs("Processing")
    .willReturn(WireMock.aResponse().withBody("{\"status\":\"shipped\"}"))
    .willSetStateTo("Completed"));

wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/api/order/status"))
    .inScenario("Order Processing")
    .whenScenarioStateIs("Completed")
    .willReturn(WireMock.aResponse().withBody("{\"status\":\"delivered\"}")));

Templating de respuestas con Handlebars

wireMock.stubFor(WireMock.get(WireMock.urlPathMatching("/api/users/([0-9]+)"))
    .willReturn(WireMock.aResponse()
        .withStatus(200)
        .withTransformers("response-template")
        .withBody("{\"id\":{{request.path.[1]}},\"name\":\"User {{request.path.[1]}}\"}")));

Usar WireMock como servidor standalone

import com.github.tomakehurst.wiremock.WireMockServer;

WireMockServer server = new WireMockServer(8089);
server.start();
server.stubFor(WireMock.get("/api/test").willReturn(WireMock.ok("hello")));
// ... correr tests ...
server.stop();

Variants

Usar anotación @WireMockTest (WireMock 3+)

@WireMockTest(httpPort = 8089)
class AnnotationTest {

    @Test
    void testWithAnnotation(WireMock wireMock) {
        wireMock.register(WireMock.get("/api/test")
            .willReturn(WireMock.ok("hello")));
        // código de test
    }
}

Usar WireMock con Spring Boot @SpringBootTest

@SpringBootTest
class SpringIntegrationTest {

    @RegisterExtension
    static WireMockExtension wireMock = WireMockExtension.newInstance()
        .options(wireMockConfig().dynamicPort())
        .build();

    @DynamicPropertySource
    static void configure(DynamicPropertyRegistry registry) {
        registry.add("external.api.url", wireMock::getRuntimeInfo);
    }
}

Best Practices

  • For a deeper guide, see Mock Network Requests with MSW.

  • Usa dynamicPort() — los puertos fijos causan conflictos cuando los tests corren en paralelo

  • Guarda bodies de respuesta grandes en archivos bajo __files/ — no inlinees JSON de 500 líneas en código de test

  • Usa verify() para asertar que tu código envió la petición correcta, no solo que recibió la respuesta correcta

  • Resetea WireMock entre tests con wireMock.resetAll() o usa la anotación @ResetWireMock

  • Usa templating de respuesta para IDs dinámicos en lugar de crear un stub por ID

  • Simula delays y errores — testear solo el happy path misses lógica de retry y fallback

Common Mistakes

  • Usar puertos fijos: el puerto 8080 puede estar en uso. Siempre usa dynamicPort().
  • No resetear entre tests: los stubs de un test filtran al siguiente. Llama resetAll() en @AfterEach.
  • Stubear demasiado broad: urlMatching(".*") atrapa cada petición, ocultando stubs faltantes para otros endpoints.
  • No verificar peticiones: stubear respuestas sin verificar la petición misses bugs en cómo tu código llama a la API.
  • Ignorar logs de WireMock: habilita logging verbose con .notifier(new ConsoleNotifier(true)) para debuggear problemas de stub matching.

FAQ

¿Cómo hago match de request bodies con JSON path?

wireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/api/orders"))
    .withRequestBody(WireMock.matchingJsonPath("$.total", WireMockGreaterThan.greaterThan(100)))
    .willReturn(WireMock.ok()));

¿WireMock puede proxyear peticiones a un servidor real?

Sí. Usa modo proxy para pasar peticiones sin stub:

wireMock.stubFor(WireMock.any(WireMock.anyUrl())
    .willReturn(WireMock.aResponse().proxiedFrom("https://real-api.example.com")));

¿Cómo simulo un connection reset?

wireMock.stubFor(WireMock.get("/api/down")
    .willReturn(WireMock.aResponse()
        .withFault(Fault.CONNECTION_RESET_BY_PEER)));

¿Puedo usar WireMock con Kotlin?

Sí. La API funciona idénticamente. Usa companion object para la extensión:

companion object {
    @RegisterExtension
    @JvmStatic
    val wireMock = WireMockExtension.newInstance()
        .options(wireMockConfig().dynamicPort())
        .build()
}

¿Cómo debuggeo por qué un stub no matchea?

Habilita console logging: .notifier(new ConsoleNotifier(true)). WireMock imprime cada petición entrante y qué stubs intentó matchear.

¿Cómo stubeo endpoints de token OAuth2 con WireMock?

Stubea el endpoint de token con un expiry fijo y usa response templating para echo del scope solicitado:

wireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/oauth/token"))
    .withRequestBody(WireMock.containing("grant_type=client_credentials"))
    .willReturn(WireMock.aResponse()
        .withHeader("Content-Type", "application/json")
        .withBody("{"access_token":"test-token","expires_in":3600}")));

Configura tu cliente HTTP para usar la URL del servidor WireMock como endpoint de token. Esto evita hitting al IdP real durante los tests mientras still ejercita el flujo de obtención de token.