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.
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 desde el propio test.
Cuándo Usar
- Tu código llama a una API REST externa, como un payment gateway, proveedor de SMS o servicio de email, y querés tests que no dependan del servicio real.
- Necesitás simular fallas — timeouts, 500s, respuestas lentas o rate limits — para ver cómo se comporta tu lógica de retry y fallback.
- 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. En ese caso, mockeá la interfaz directamente con Mockito.
- Estás haciendo load testing. WireMock agrega overhead, así que usá un entorno real.
- Estás testeando interacciones con base de datos. Para eso, usá 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();
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 — activá salida detallada con
.notifier(new ConsoleNotifier(true))cuando un stub no matchea.
Preguntas frecuentes
¿Cómo matcheo 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 pasa cualquier request que no tenga 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 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.
RecipeJUnit5 Soft Assertions con AssertJ
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
Cómo gestionar fixtures de test con patrones factory, hooks de setup/teardown y datos deterministas para tests unitarios e integración confiables en Python, JavaScript y Java.