Pruebas de API en Java con WireMock

Pruebas de API en Java con WireMock: Una Guía Completa

1. Introducción

Probar dependencias externas, como las APIs REST, puede ser un desafío al desarrollar aplicaciones web. Realizar llamadas de red es lento e impredecible, ya que los servicios de terceros pueden no estar disponibles o devolver datos inesperados. Debemos identificar un método robusto para simular servicios externos que garantice pruebas de aplicación consistentes y confiables. Aquí es donde entra en juego WireMock.

WireMock es un potente servidor de simulación HTTP que nos permite hacer stubs y verificar solicitudes HTTP. Ofrece un entorno de prueba controlado, asegurando que nuestras pruebas de integración sean rápidas, repetibles e independientes de sistemas externos.

En este tutorial, exploraremos cómo integrar WireMock en un proyecto de Spring Boot y cómo utilizarlo para escribir pruebas completas.

2. Dependencia de Maven

Para utilizar WireMock con Spring Boot, debemos incluir la dependencia de wiremock-spring-boot en nuestro archivo pom.xml:

<dependency>
    <groupId>org.wiremock.integrations</groupId>
    <artifactId>wiremock-spring-boot</artifactId>
    <version>3.9.0</version>
    <scope>test</scope>
</dependency>

Esta dependencia proporciona una integración sin problemas entre WireMock y el marco de pruebas de Spring Boot.

3. Escribiendo una Prueba Básica de WireMock

Antes de abordar escenarios más complejos, comenzaremos escribiendo una prueba simple de WireMock. Debemos garantizar que nuestra aplicación de Spring Boot pueda interactuar correctamente con una API externa. Usando las anotaciones @SpringBootTest y @EnableWireMock, habilitamos WireMock en nuestro entorno de prueba. Luego, podemos definir un caso de prueba simple para verificar el comportamiento de la API:

<SpringBootTest(classes = SimpleWiremockTest.AppConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)>
@EnableWireMock
class SimpleWiremockTest {
    @Value("${wiremock.server.baseUrl}")
    private String wireMockUrl;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void givenWireMockStub_whenGetPing_thenReturnsPong() {
        stubFor(get("/ping").willReturn(ok("pong")));

        ResponseEntity<String> response = restTemplate.getForEntity(wireMockUrl + "/ping", String.class);

        Assertions.assertEquals("pong", response.getBody());
    }

    @SpringBootApplication
    static class AppConfiguration {}
}

En esta prueba, usamos la anotación @EnableWireMock para iniciar un servidor WireMock embebido para el entorno de prueba. La anotación @Value("${wiremock.server.baseUrl}") obtiene la URL base de WireMock del archivo de propiedades. El método de prueba stuba un endpoint /ping para devolver “pong” con un código de estado HTTP 200. Luego, hacemos una solicitud HTTP real usando TestRestTemplate y verificamos que el cuerpo de la respuesta coincida con el valor esperado. Esto asegura que nuestra aplicación se comunique correctamente con el servicio externo simulado.

4. Haciendo la Prueba Más Compleja

Ahora que tenemos una prueba básica, extenderemos nuestro ejemplo para simular una API REST que devuelva respuestas JSON y maneje varios códigos de estado. Esto nos ayudará a verificar cómo nuestra aplicación procesa diferentes comportamientos de API.

4.1. Stubbing de una Respuesta JSON

Un escenario común en las APIs REST es devolver respuestas JSON estructuradas. También podemos simular este caso usando stubs de WireMock:

@Test
void givenWireMockStub_whenGetGreeting_thenReturnsMockedJsonResponse() {
    String mockResponse = "{\"message\": \"Hello, Baeldung!\"}";
    stubFor(get("/api/greeting")
      .willReturn(okJson(mockResponse)));

    ResponseEntity<String> response = restTemplate.getForEntity(wireMockUrl + "/api/greeting", String.class);

    Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
    Assertions.assertEquals(mockResponse, response.getBody());
}

En esta prueba, stubbamos una solicitud GET a /api/greeting que devuelve una respuesta JSON que contiene un mensaje de saludo. Luego solicitamos al servidor WireMock verificar que el código de estado de respuesta sea 200 OK y que el cuerpo coincida con la estructura JSON esperada.

4.2. Simulando una Respuesta de Error

Sabemos que no siempre las cosas salen como deberían, especialmente en el desarrollo web, y algunas llamadas externas pueden devolver errores. Para estar preparados, también podemos simular mensajes de error para que nuestra aplicación responda apropiadamente a esto:

@Test
void givenWireMockStub_whenGetUnknownResource_thenReturnsNotFound() {
    stubFor(get("/api/unknown").willReturn(aResponse().withStatus(404)));

    ResponseEntity<String> response = restTemplate.getForEntity(wireMockUrl + "/api/unknown", String.class);

    Assertions.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}

5. Inyectando el Servidor WireMock

En escenarios más complejos, podemos necesitar gestionar múltiples instancias de WireMock o configurarlas con ajustes específicos. WireMock nos permite inyectar y configurar múltiples servidores de WireMock usando la anotación @InjectWireMock. Esto es especialmente útil cuando nuestra aplicación interactúa con numerosos servicios externos y queremos simular cada uno de forma independiente.

5.1. Inyectando un Solo Servidor WireMock

Comencemos inyectando un solo servidor WireMock en nuestra clase de prueba. Este método es útil cuando se simula un solo servicio externo:

@SpringBootTest(classes = SimpleWiremockTest.AppConfiguration.class,
  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableWireMock({
    @ConfigureWireMock(name = "user-service", port = 8081),
})
public class InjectedWiremockTest {
    @InjectWireMock("user-service")
    WireMockServer mockUserService;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void givenEmptyUserList_whenFetchingUsers_thenReturnsEmptyList() {
        mockUserService.stubFor(get("/users").willReturn(okJson("[]")));

        ResponseEntity<String> response = restTemplate.getForEntity(
          "http://localhost:8081/users",
          String.class);

        Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
        Assertions.assertEquals("[]", response.getBody());
    }
}

A diferencia del enfoque anterior, donde WireMock se habilitó a nivel de clase de prueba usando @EnableWireMock sin inyección explícita, este método permite un control más granular al inyectar una instancia de WireMock designada. La anotación @ConfigureWireMock define explícitamente el nombre y puerto de la instancia de WireMock, facilitando la gestión de múltiples servicios externos dentro de diferentes casos de prueba.

La anotación @InjectWireMock("user-service") nos permite acceder directamente a la instancia de WireMockServer para configurar y gestionar su comportamiento dinámicamente dentro de nuestros métodos de prueba.

5.2. Inyectando Múltiples Servidores WireMock

En casos donde nuestra aplicación interactúe con múltiples servicios externos, quizás necesitemos simular múltiples APIs usando instancias separadas de WireMock. WireMock permite configurar y especificar diferentes nombres y puertos para cada instancia:

@SpringBootTest(classes = SimpleWiremockTest.AppConfiguration.class,
  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableWireMock({
    @ConfigureWireMock(name = "user-service", port = 8081),
    @ConfigureWireMock(name = "product-service", port = 8082)
})
public class InjectedWiremockTest {
    @InjectWireMock("user-service")
    WireMockServer mockUserService;

    @InjectWireMock("product-service")
    WireMockServer mockProductService;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void givenUserAndProductLists_whenFetchingUsersAndProducts_thenReturnsMockedData() {
        mockUserService.stubFor(get("/users")
          .willReturn(okJson("[{\"id\": 1, \"name\": \"John\"}]")));
        mockProductService.stubFor(get("/products")
          .willReturn(okJson("[{\"id\": 101, \"name\": \"Laptop\"}]")));

        ResponseEntity<String> userResponse = restTemplate
          .getForEntity("http://localhost:8081/users", String.class);
        ResponseEntity<String> productResponse = restTemplate
          .getForEntity("http://localhost:8082/products", String.class);

        Assertions.assertEquals(HttpStatus.OK, userResponse.getStatusCode());
        Assertions.assertEquals("[{\"id\": 1, \"name\": \"John\"}]", userResponse.getBody());

        Assertions.assertEquals(HttpStatus.OK, productResponse.getStatusCode());
        Assertions.assertEquals("[{\"id\": 101, \"name\": \"Laptop\"}]", productResponse.getBody());
    }
}

Aislamos los servicios, asegurando que los cambios en un servidor simulado no interfieran con los demás. Al inyectar múltiples instancias de WireMock, podemos simular completamente interacciones complejas de servicios, haciendo que nuestras pruebas sean más precisas y confiables. Este método es particularmente beneficioso en arquitecturas de microservicios, donde diferentes componentes se comunican con varios servicios externos.

6. Conclusión

WireMock es una herramienta poderosa para probar dependencias externas en una aplicación de Spring Boot. En este artículo, creamos pruebas confiables, repetibles e independientes sin depender de servicios de terceros reales. Comenzamos con una prueba simple y la evolucionamos hacia escenarios más avanzados, incluyendo la inyección de múltiples servidores WireMock.

Con estas técnicas, podemos asegurar que nuestras aplicaciones manejen correctamente las respuestas de la API externa, ya sea que devuelvan datos esperados o errores. Las implementaciones de todos los ejemplos y fragmentos de código se pueden encontrar aquí.

Consejos Prácticos:

  • Siempre utiliza mocks para pruebas de integración con servicios externos, esto evita problemas de disponibilidad.
  • Practica la gestión de stubs tanto para respuestas valiosas como para situaciones de error, esto te ayudará a construir aplicaciones más robustas.
  • Mantén tu configuración de WireMock clara y organiza tus pruebas para facilitar el mantenimiento a futuro.
  • Conservar todos los datos, la información y las URL originales.