StackPractices
intermediate Por Mathias Paulenko

Stubear 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.

Temas: testing

Overview

sequenceDiagram diagram: participant Test

WireMock es una librería de Java que levanta un servidor HTTP real dentro del proceso de tus tests. En lugar de pegarle a una API de terceros, tu código habla con WireMock y vos decidís exactamente qué devuelve cada endpoint. Podés configurar códigos de estado, headers, bodies, delays artificiales e incluso comportamiento stateful — todo sin salir del JVM del test.

Recurrí a WireMock en tres o cuatro proyectos — mayormente integraciones con payment gateways donde pegarle a la API real en los tests no era opción. Es mi herramienta go-to cuando el código bajo test hace HTTP calls reales y necesito controlar exactamente qué vuelve. Para estrategias más amplias de testing de integración, mirá nuestra recipe de integration testing strategies. Si estás testeando servicios containerizados como bases de datos o message brokers, consultá nuestra recipe de Testcontainers.

Cuándo Usar

  • Tu código llama a una API REST externa — payment gateway, proveedor de SMS, servicio de email, ese tipo de cosas — y querés tests que no dependan del servicio real.
  • Necesitás simular fallas — timeouts, 500s, respuestas lentas, rate limits — para que tu lógica de retry y fallback se ejercite de verdad.
  • Querés verificar el request exacto que envía tu código, no solo la respuesta que recibe.
  • Estás construyendo un webhook receiver y necesitás un sender controlado sin levantar uno real.
  • Corrés tests de integración en CI y no podés depender del acceso a red.

Cuándo NO Usar

  • Estás testeando tus propios endpoints. Para Spring, usá MockMvc o WebTestClient.
  • Solo necesitás unit tests para lógica de negocio. Mockeá la interfaz con Mockito — no necesitás un servidor HTTP real para esos.
  • Estás haciendo load testing. WireMock agrega overhead — usá un entorno real para load.
  • Estás testeando interacciones con base de datos. Reach for Testcontainers con una base real.

Solución

Setup con JUnit 5

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

Stub básico con la extensión de JUnit 5

import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
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"));
    }
}

Este test arranca WireMock en un puerto aleatorio, registra un stub para /api/users/1 y luego le hace un HTTP call real.

Stub con body JSON desde archivo

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

Colocá products-response.json bajo 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 requests

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

Stubs 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\"}")));

Response templating 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]}}\"}")));

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();

Explicación

WireMock funciona embebiendo un servidor Jetty HTTP dentro del JVM del test. Cuando llamás stubFor(), WireMock registra un mapping en su store interno. Cuando el código bajo test hace un HTTP request a la URL de WireMock, el servidor matchea el request contra los stubs registrados usando URL path, method, headers y body patterns. El primer stub que matchea gana — el orden de registro importa cuando los stubs se superponen.

La diferencia clave entre WireMock y Mockito es el scope. Mockito mockea interfaces de Java — intercepta method calls a nivel lenguaje. WireMock mockea HTTP — levanta un servidor real que habla el protocolo. Usá Mockito cuando tu código llama a una interfaz que controlás. Usá WireMock cuando tu código habla con un servicio externo over HTTP — HttpClient, RestTemplate, OkHttp, no importa qué client uses.

Vi equipos que intentaron mockear HttpClient con Mockito y terminaron con tests frágiles que pasan pero no atrapan problemas reales de serialización o headers. WireMock los atrapa porque el HTTP call es real — solo el servidor del otro lado es fake.

WireMock vs Mockito vs MockWebServer

FeatureWireMockMockitoMockWebServer
ScopeStubbing a nivel HTTPMocking a nivel interfazStubbing a nivel HTTP
HTTP serverServidor Jetty realSin servidorServidor OkHttp real
Response templatingHandlebarsN/AN/A
Stubs statefulEscenariosN/AN/A
Simulación de delayBuilt-inN/AManual
Verificación de requestsURL, body, headersArgs de métodoRequests grabados
Standalone/DockerSíNoNo
LenguajeJava, Kotlin, cualquier JVMJava, KotlinJava, Kotlin
Mejor paraStubear APIs externasUnit testing de interfacesHTTP mocking simple

Prefiero WireMock sobre MockWebServer para cualquier cosa más allá de los stubs más simples. MockWebServer está bien para un test rápido de “devolver 200 para esta URL”, pero los escenarios, templating y recording mode de WireMock ahorran tiempo real cuando la API que estás mockeando tiene comportamiento complejo.

Recording mode

WireMock puede grabar calls reales a una API y generar stubs a partir de ellas. Uso esto cuando integro con una API por primera vez y quiero capturar respuestas reales:

wireMock.startRecording("https://real-api.example.com");
// ... correr código que llama a la API real a través de WireMock ...
List<StubMapping> recordedStubs = wireMock.stopRecording().getStubMappings();

El recording mode proxya los requests a la API real, captura las respuestas y crea stub mappings que podés guardar y replayear después. Es la forma más rápida que encontré para bootstrappear una suite de tests para una API enredada.

WireMock en Docker

Para tests que necesitan una instancia compartida de WireMock across dos o más servicios o lenguajes, corrélo como container Docker:

docker run -d --name wiremock -p 8080:8080 \
  wiremock/wiremock:3.9.1 --verbose

Montá stubs y archivos vía volumes:

docker run -d -p 8080:8080 \
  -v ./stubs:/home/wiremock/mappings \
  -v ./responses:/home/wiremock/__files \
  wiremock/wiremock:3.9.1

Esto es útil para contract testing across microservicios — cada equipo puede compartir la misma imagen Docker de WireMock con stubs pre-cargados.

Variantes

Usando la 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")));
        // test code
    }
}

WireMock con Spring Boot

@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);
    }
}

Buenas Prácticas

  • Dejá que WireMock elija el puerto con dynamicPort(). Los puertos fijos generan conflictos cuando los tests corren en paralelo.
  • Guardá bodies grandes en archivos bajo __files/ en lugar de inlinear JSON largo en Java.
  • Usá verify() para chequear que tu código envió el request correcto, no solo que recibió una respuesta.
  • Reseteá WireMock entre tests con wireMock.resetAll() o la anotación @ResetWireMock.
  • Usá response templating para IDs dinámicos, así no necesitás un stub por cada valor.
  • Siempre testeá los caminos de falla — delays, errores y respuestas lentas — o tu lógica de retry y fallback queda sin testear.

Errores Comunes

  • Usar puertos fijos — el puerto 8080 puede estar en uso. Dejá que WireMock elija.
  • No resetear entre tests — los stubs de un test pueden filtrarse al siguiente. Llamá resetAll() en @AfterEach.
  • Stubs demasiado amplios — urlMatching(".*") atrapa todo y oculta stubs faltantes.
  • No verificar requests — stubear respuestas sin verificar el request te hace perder bugs en cómo tu código llama a la API.
  • Ignorar los logs de WireMock — cuando un stub no matchea, prendé salida detallada con .notifier(new ConsoleNotifier(true)) y WireMock te va a decir exactamente qué probó.

See Also

Preguntas frecuentes

¿Cuál es la mejor forma de matchear request bodies con JSON path?
wireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/api/orders"))
    .withRequestBody(WireMock.matchingJsonPath("$.total", WireMockGreaterThan.greaterThan(100)))
    .willReturn(WireMock.ok()));
¿Puede WireMock proxyar requests a un servidor real?

Sí. El proxy mode forwardea cualquier request que no matchee un stub a un servidor real:

wireMock.stubFor(WireMock.any(WireMock.anyUrl())
    .willReturn(WireMock.aResponse().proxiedFrom("https://real-api.example.com")));
¿Por qué simularía 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 es la misma. En Kotlin, usá un 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 hace match?

Activá el console logging:

wireMockConfig()
    .notifier(new ConsoleNotifier(true))

WireMock imprime cada request entrante y los stubs contra los que intentó matchear.