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.
Overview
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á
MockMvcoWebTestClient. - 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
| Feature | WireMock | Mockito | MockWebServer |
|---|---|---|---|
| Scope | Stubbing a nivel HTTP | Mocking a nivel interfaz | Stubbing a nivel HTTP |
| HTTP server | Servidor Jetty real | Sin servidor | Servidor OkHttp real |
| Response templating | Handlebars | N/A | N/A |
| Stubs stateful | Escenarios | N/A | N/A |
| Simulación de delay | Built-in | N/A | Manual |
| Verificación de requests | URL, body, headers | Args de método | Requests grabados |
| Standalone/Docker | Sí | No | No |
| Lenguaje | Java, Kotlin, cualquier JVM | Java, Kotlin | Java, Kotlin |
| Mejor para | Stubear APIs externas | Unit testing de interfaces | HTTP 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
- Documentación de WireMock — los docs oficiales, cubriendo cada opción de config y feature que WireMock trae.
- Extensión de WireMock para JUnit 5 — guía oficial de integración con JUnit 5.
- Response templating de WireMock — referencia de templating Handlebars para respuestas dinámicas.
- Imágenes Docker de WireMock — imágenes Docker oficiales para setups standalone y Docker Compose.
- Recipe de JUnit 5 assertions — nuestra guía de patrones de assertions de JUnit 5 incluyendo soft assertions.
- Recipe de MSW — Mock Service Worker para proyectos JavaScript/TypeScript, el equivalente a WireMock en el ecosistema JS.
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.
Recursos Relacionados
Testcontainers para Tests de Integración en Java
Cómo usar Testcontainers en JUnit5 para levantar contenedores reales de Postgres, Redis y Kafka para tests de integración confiables y reproducibles.
RecipeSoft Assertions con AssertJ en JUnit5: todos los fallos
Cómo usar AssertJ soft assertions en JUnit5 para recolectar múltiples fallos de aserción en un solo test en lugar de detenerse en el primer fallo.
RecipeDiseñar Tests de Integración Efectivos para Sistemas
Cómo escribir tests de integración que verifiquen interacciones de componentes usando test containers, contratos de API, consumer-driven contracts y contract testing en Java, TypeScript y Python.
RecipeMockear Peticiones de Red con MSW
Cómo usar Mock Service Worker (MSW) para interceptar peticiones de red en tests y desarrollo de JavaScript, incluyendo mocking de REST y GraphQL.
RecipeAPI Mocking para Testing
Construye tests confiables mockeando APIs externas con WireMock, MockServer y MSW para eliminar flakiness y testear casos edge.
RecipeConfigurar Fixtures de Test (pytest, Jest, JUnit)
Cómo crear, acotar y limpiar fixtures de test con pytest, hooks de Jest y el ciclo de vida de JUnit 5 — más conftest.py, factories de fixtures y patrones de teardown.