Servidor mock para imitar peticiones y respuestas de servicios HTTP con WireMock

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

Los microservicios aportan varias ventajas pero también algunos inconvenientes que si no son manejados generan sus propios problemas. Una dificultad de los servicios por las dependencias entre ellos es poder desarrollarlos y probarlos en local, algunos microservicios son complejos con dependencia sobre bases de datos, sistemas de envío de mensajes u otros servicios. Si un microservicio necesita iniciar en local o en entorno todas sus dependencias el desarrollo se vuelve complejo y lento. Para facilitar el desarrollo una opción es utilizar un servidor mock que imite las respuestas para las peticiones que se necesite de uno o varios servicios.

Java

Una aplicación diseñada como una colección de microservicios se compone de múltiples de ellos, unos microservicios son consumidos por otros y a su vez un microservicio consume otros uno o varios.

Algunas aplicaciones son diseñadas para ofrecer su funcionalidad a través de un API desde el primer momento por su independencia de los clientes que hagan uso de ella. Tener un API permite dar soporte a los múltiples clientes ya sean directamente desde el navegador web, una aplicación nativa de un dispositivo como un teléfono inteligente o incluso para ofrecer a tercera partes de modo que realicen integraciones y automatizaciones según sus necesidades.

Las ventajas de los microservicios son varios como los anteriores junto a algunos otros adicionales, sin embargo, añaden otros problemas, principalmente el mayor número de elementos que los hacen más complejos comparado con una aplicación monolítica.

El servidor mock

Muchas aplicaciones se basan en microservicios REST haciendo uso del protocolo HTTP y JSON como formato de datos. Un servidor mock es simplemente un servidor web que en caso de los microservicios es utilizado para programar las respuestas para las peticiones que se le hagan según el endpoint invocado, variables en el path, parámetros o cabeceras. Las respuestas programadas incluyen el código de estado, cabeceras devueltas y datos del cuerpo.

Un servidor de imitación o mock facilita el desarrollo de los microservicios de forma independiente y las pruebas. El servidor mock elimina la dependencia de un servicio real junto con todo el entorno de ejecución que necesite que en el caso de algunos llega a ser notablemente complejo si incluye base de datos, sistemas de mensajería u otros servicios. El servicio es sustituido por una imitación que devuelve las respuestas programadas para cada una de las peticiones.

Otro caso de uso de un servidor mock es permitir realizar pruebas de código o convertir pruebas de integración en unitarias. Otro uso de un servidor mock es que permite centrarse en el desarrollo de un servicio sin necesidad de usar servicios reales incluso antes de que estos estén implementados si su interfaz está definida.

Uno de los potenciales riesgos de utilizar un servidor mock es que este no se ajuste a la realidad del servicio real cuando este contenga cambios incompatibles. Un servidor mock permite simular las respuestas de un servicio HTTP lo que facilita las pruebas unitarias de la parte cliente, sin embargo, esto no asegura que el servidor al realizar en las pruebas de integración o en producción cumpla con el contrato que el cliente espera de su API. Para asegurar que el servidor soporta las peticiones esperadas por la parte cliente y devuelve los datos esperados otra forma de pruebas son las pruebas de contrato o contract testing con Pact, una herramienta de pruebas de contrato que soporta el lenguaje Java entre otros.

Opciones de servidores mock

Como cualquier otro tipo de herramienta hay múltiples opciones entre las que elegir. La principal característica de todo servidor mock es permitir programar las respuestas según las peticiones, sus diferencias está en el lenguaje de programación en el que están implementadas y su entorno de ejecución necesario así como su tipo de licencia. Algunas ofrecen programar las respuestas a través de una API del lenguaje de programación para el que están destinadas.

Hay muchas opciones de servidor mock algunas conocidas son MockServer, WireMock, Imposter o Prism. Varias implementadas con JavaScript, otras en Java y algunas incluso se ofrecen en forma de software como servicio para delegar el mantenimiento de la herramienta en una tercera parte.

Características de WireMock

WireMock es una opción bastante conocida de servidor para hacer mocking. Ofrece bastante flexibilidad en la forma de aprovisionar las respuestas programadas ya sea a través de un archivo de configuración, peticiones REST una vez iniciado el servidor mock o de forma programática mediante una API de Java. También es bastante flexible en su forma de ejecución pudiendo ser como una aplicación Java independiente, de forma embebida como parte de una aplicación Java como sería el caso de querer utilizarlo para realizar pruebas unitarias o como un contenedor de Docker.

Ofrece una potente definición de correspondencia entre las peticiones realizadas a través de las URLs, métodos, cabeceras, cookies con diferentes estrategias, también ofrece soporte para generar respuestas en formato JSON o XML pudiendo utilizar plantillas para crear respuestas dinámicas según la petición de entrada.  Otras características que ofrece es soporte para HTTPS, hacer de intermediario o proxy entre la aplicación y el servicio real para peticiones que no están programadas, permitir grabar las respuestas obtenidas por la funcionalidad de proxy, simular errores como tiempos de respuesta elevados y crear flujos de peticiones con escenarios que dependen de estado e interacciones previas.

Es simple de iniciar y configurar, tiene una documentación suficiente para aprender sus conceptos básicos, configuración junto la documentación completa de la API REST y empezar a usarlo en poco tiempo, está implementado en Java que lo hace adecuado si es el entorno de ejecución utilizado para los microservicios.

Ejemplo de prueba de WireMock

De forma oficial el proyecto ofrece un archivo jar ejecutable que inicia el servidor web de WireMock de forma independiente, una vez iniciado expone una API REST a través de la cual es posible aprovisionar las respuestas, el aprovisionamiento y configuración también es posible realizarlo mediante parámetros de inicio. A partir de este archivo jar ejecutable es posible crear una imagen de Docker con el servicio para ejecutarlo en forma de contenedor o en pruebas unitarias o de integración con Testcontainers, alguna persona ya ha creado una imagen de Docker de WireMock.

Como aplicación independiente

Este es el comando de inicio de WireMock como aplicación independiente.

1
2
$ java -jar wiremock-jre8-standalone-2.29.1.jar 

wiremock.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
 /$$      /$$ /$$                     /$$      /$$                     /$$      
| $$  /$ | $$|__/                    | $$$    /$$$                    | $$      
| $$ /$$$| $$ /$$  /$$$$$$   /$$$$$$ | $$$$  /$$$$  /$$$$$$   /$$$$$$$| $$   /$$
| $$/$$ $$ $$| $$ /$$__  $$ /$$__  $$| $$ $$/$$ $$ /$$__  $$ /$$_____/| $$  /$$/
| $$$$_  $$$$| $$| $$  \__/| $$$$$$$$| $$  $$$| $$| $$  \ $$| $$      | $$$$$$/ 
| $$$/ \  $$$| $$| $$      | $$_____/| $$\  $ | $$| $$  | $$| $$      | $$_  $$ 
| $$/   \  $$| $$| $$      |  $$$$$$$| $$ \/  | $$|  $$$$$$/|  $$$$$$$| $$ \  $$
|__/     \__/|__/|__/       \_______/|__/     |__/ \______/  \_______/|__/  \__/

port:                         8080
enable-browser-proxying:      false
disable-banner:               false
no-request-journal:           false
verbose:                      false
wiremock.out

Por defecto el servidor mock se inicia en el puerto 8080. Con las siguientes peticiones REST es posible aprovisionar manualmente las respuestas, estas peticiones utilizan la API REST de WireMock. También es posible realizar el aprovisionamiento con archivos de configuración creando una carpeta en el directorio de trabajo de nombre mappings creando archivos con extensión json con el contenido del JSON de cada uno de los mappings.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ curl -v -X POST http://localhost:8080/__admin/mappings/import --data '{
    "mappings": [{
        "request": {
            "method": "GET",
            "url": "/message/1"
        },
        "response": {
            "status": 200,
            "jsonBody": {
                "id": 1,
                "text": "Hello World!"
            }
        }
    }]
}'

$ curl -v -X POST http://localhost:8080/__admin/mappings/import --data '{
    "mappings": [{
        "request": {
            "method": "POST",
            "url": "/message",
            "bodyPatterns" : [{
                "equalToJson" : {
                    "id": 1,
                    "text": "Hello World!"
                }
            }]
        },
        "response": {
            "status": 200
        }
    }]
}'
curl-wiremock-provision.sh

Una vez aprovisionado el servidor mock con las respuestas deseadas al realizar peticiones al servidor de WireMock si estás coinciden se devuelven las respuestas, la respuestas incluye el código de estado, las cabeceras y los datos de respuesta tal como fueron aprovisionados. En este caso las peticiones se hacen con el comando curl que simulan las peticiones de una aplicación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ curl -v http://localhost:8080/message/1
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /message/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.78.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Matched-Stub-Id: 9acc8318-dff3-4d18-9522-56861eff0ca3
< Vary: Accept-Encoding, User-Agent
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
{"id":1,"text":"Hello World!"}

$ curl -v -X POST http://localhost:8080/message --data '{"id":1,"text":"Hello World!"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /message HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.78.0
> Accept: */*
> Content-Length: 30
> Content-Type: application/x-www-form-urlencoded
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Matched-Stub-Id: 7ef3a405-4435-417d-8579-57497d149f29
< Vary: Accept-Encoding, User-Agent
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
curl-wiremock-request.sh

En caso de que la petición no coincida con una aprovisionada se devuelve en error 404.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ curl -v -H "Accept: application/json" http://localhost:8080/message/2
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /message/2 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.78.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< Content-Type: text/plain
< Transfer-Encoding: chunked
< 

                                               Request was not matched
                                               =======================

-----------------------------------------------------------------------------------------------------------------------
| Closest stub                                             | Request                                                  |
-----------------------------------------------------------------------------------------------------------------------
                                                           |
GET                                                        | GET
/message/1                                                 | /message/2                                          <<<<< URL does not match
                                                           |
                                                           |
-----------------------------------------------------------------------------------------------------------------------
* Connection #0 to host localhost left intact
curl-wiremock-nomatch.sh

Modificando la aplicación para que las peticiones las haga al servidor de WireMock la aplicación es posible desarrollarla o probarla sin necesidad del servicio real y sus dependencias.

Embebido en una aplicación para hacer pruebas unitarias

En el caso de utilizar WireMock para realizar pruebas unitarias el servidor de WireMock ha de iniciarse y aprovisionarse en el contexto de las pruebas, en este caso para las pruebas unitarias con teses de Junit5.

En este ejemplo se crea una interfaz de un cliente de una API REST con Retrofit, a partir de esta interfaz Retrofit permite crear una instancia de un objeto que a través de sus métodos y parámetros permite hacer llamadas al servicio REST mediante código Java eliminando los detalles de que en realidad hace una petición HTTP.

1
2
3
4
5
6
7
8
9
package io.github.picotodev.blogbitix.javawiremock;

...

public interface Service {

    @GET("/message/{id}")
    Call<String> message(@Path("id") Long id);
}
Service.java

La aplicación en su código crea una instancia del cliente del servicio REST e invoca sus métodos de llamada, dado que el cliente realiza operaciones de red al ejecutarlo al hacer la prueba unitaria si el servicio no está iniciado la comunicación fallará produciendo errores.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package io.github.picotodev.blogbitix.javawiremock;

...

public class Main {

    private Service service;

    public Main() {
        this.service = buildService();
    }

    public String getMessage(Long id) throws IOException {
        return service.message(id).execute().body();
    }

    public Service buildService() {
        OkHttpClient client = new OkHttpClient.Builder()
                .build();

        Retrofit retrofit = new Retrofit.Builder()
                .client(client)
                .addConverterFactory(ScalarsConverterFactory.create())
                .baseUrl("http://localhost:8080/").build();

        return retrofit.create(Service.class);
    }

    public static void main(String[] args) {
    }
}
Main.java

Es en este punto donde entra WireMock que permite simular ese servicio, en este ejemplo de prueba unitaria se inicia el servidor de WireMock, se aprovisiona con la petición esperada y respuesta desea a devolver. Se ejercita el código que se desea probar en este caso el método getMessage de la clase Main que en su implementación hace uso del cliente del servicio REST con Retrofit y que en la prueba invocará al servidor de WireMock. Finalmente, se comprueba que la respuesta del clase Main coincida con la esperada.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package io.github.picotodev.blogbitix.javawiremock;

...

public class MainTest {

    private static WireMockServer wireMockServer;

    @BeforeAll
    static void beforeAll() {
        wireMockServer = buildWireMockServer();
    }

    @AfterAll
    static void afterAll() {
        wireMockServer.stop();
    }

    @Test
    void test() throws Exception {
        // given
        stubFor(get(urlEqualTo("/message/1"))
                .willReturn(aResponse()
                        .withHeader("Content-Type", "appication/json")
                        .withBody("{\"id\": 1, \"text\": \"Hello World!\"}")));

        // when
        String response = new Main().getMessage(1L);

        // then
        assertEquals("{\"id\": 1, \"text\": \"Hello World!\"}", response);
    }

    private static WireMockServer buildWireMockServer() {
        WireMockServer wireMockServer = new WireMockServer(WireMockConfiguration.options().port(8080));
        wireMockServer.start();
        return wireMockServer;
    }
}
MainTest.java

Este el archivo de Gradle con las dependencias.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
plugins {
    id 'application'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'

    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
    testImplementation 'com.github.tomakehurst:wiremock-jre8:2.29.1'
}

application {
    mainClass = 'io.github.picotodev.blogbitix.javawiremock.Main'
}

tasks.named('test') {
    useJUnitPlatform()
}
build.gradle

En este caso WireMock se ha usado de forma directa, en el caso de utilizar Spring Boot uno de los proyectos de Spring proporciona librerías para facilitar y hacer compatible el uso de WireMock con JUnit y Spring.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew test


Comparte el artículo: