Introducción a Mountebank para la creación de servidores dobles de test

0
9854

En este tutorial haremos una introducción a la herramienta Mountebank para la creación de servidores que actúen como dobles de test (mock servers) de correo electrónico y de HTTP para ser utilizados en tests de una aplicación Java

0. Índice de contenidos

1. Introducción

En muchas ocasiones resulta util tener la posibilidad de utilizar servidores simulados para realizar pruebas de nuestras aplicaciones. La herramienta open source Mounteback permite la creación y configuración de forma sencilla de servidores que actúen como dobles de test en dichas pruebas. La versión 1.2.122 utilizada en este tutorial permite simular servidores que utilicen los protocolos SMTP, HTTP, HTTPS y TCP.

El objetivo de este tutorial es crear unos sencillos ejemplos que permitan ver las capacidades de esta herramienta desde una aplicación Java

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.5 Ghz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Yosemite 10.10.5
  • Entorno de desarrollo: Eclipse Mars 4.5.0
  • Gestión de dependencias: Apache Maven 3.3.3
  • Java: JDK 1.8
  • Mountebank: Self-contained archives 1.2.122

3. Instalación y puesta en marcha del servidor Mountebank

Mountbank es una herramienta open source para la creación de servidores dobles de test que corre sobre un servidor node.js 0.10. Se puede instalar como un componente de un servidor existente o descargar en una versión empaquetada que ya incluye dicho servidor embebido. Cualquier opción es igualmente válida, pero para no complicar en exceso este tutorial, utilizaremos la versión embebida.

Para hacer funcionar dicha versión hay que descomprimir el fichero que podemos descargar de la página web de mountebank (http://www.mbtest.org/y ejecutar el comando «mb» que hay en su interior. Si todo va bien, veremos un mensaje similar a este en la consola, indicando que el servidor está listo para recibir peticiones

info: [mb:2525] mountebank v1.2.122 now taking orders - point your browser to http://localhost:2525 for help

En este momento nuestro servidor está preparado para escuchar. Ahora, debemos cargar los dobles de test en el servidor. Estos dobles de test son llamados imposters. La forma de configurar dichos imposters es mediante una sencilla interfaz REST que ofrece el servidor mounteback instalado en el puerto 2525. La interfaz nos ofrece los típicos servicios CRUD (acrónimo de Crear, Leer, Actualizar y Borrar, por sus siglas en inglés) en el path «/imposters»

Para crear un imposter se debe utilizar el método POST, adjuntando la definición de dicho servidor en formato JSON. Según la documentación de la API, el único atributo obligatorio sería «protocol», sin embargo es recomendable incluir también el puerto, ya que en caso contrario, mountebank creará el servidor en un puerto aleatorio, y esto, dado que el método POST no garantiza la entrega, puede ser un problema que genere duplicados en el servidor si se lanzan reintentos tras un timeout. Con esto, una definición básica de un servidor doble de test quedaría de la siguiente forma:

{
    "port": 8484,
    "protocol": "http"
}

La definición anterior generaría un servidor doble de test que responda al protocolo HTTP en el puerto 8484, cuya respuesta ante cualquier petición sería un cuerpo vacío con código de estatus 200. Para probar la funcionalidad de dicho servidor habría que hacer una llamada en el path «/test».

Una vez creado el servidor, este queda registrado en el path «/imposters/PUERTO«, donde PUERTO sería 8484 en el ejemplo anterior. Este path será el utilizado si se quiere consultar los detalles del servidor usando GET o borrarlo usando DELETE. La opción de actualizarlo no está disponible para servidores individuales, quedando sólo la opción de actualizar todos los imposters a la vez usando PUT desde el path «/imposters».

Estos imposters se podrían crear de forma externa a la aplicación utilizando una herramienta como «curl». Sin embargo, para aumentar la automatización de los test, vamos a integrar la creación de los servidores dobles de test en nuestra aplicación, una vez creados se realizarán los test correspondientes y al finalizar, los imposters serán eliminados.

4. Configuración del proyecto Java

Este tutorial utiliza un proyecto Java haciendo uso de Maven como gestor de dependencias y compilación. El fichero «pom.xml» queda de la siguiente forma:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.autentia.tutoriales</groupId>
    <artifactId>mounteback</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>mounteback</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-email</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.6.1</version>
        </dependency>
    </dependencies>
</project>

5. Test de integración con un servidor SMTP doble de test

En este apartado vamos a ver cómo crear un servidor SMTP doble de test. En este caso vamos a crear una sencilla clase de Java encargada de enviar un correo. En un entorno real la lógica de negocio sería más compleja, pero se ha pretendido mostrar la configuración lo más sencilla posible para centrar el interés en la parte de mountebank.

ExampleEmailSender.java
public class ExampleEmailSender {
    public void sendEmail(Email email) throws EmailException{
        email.send();
    }
}

A continuación se muestra el código de la clase de test donde se prueba la funcionalidad de la clase de negocio:

ExampleEmailSenderTest.java
ppublic class ExampleEmailSenderTest {

    private static final String MOUNTEBANK_HOST = "localhost";

    private static final String TEST_EMAIL_FROM = "test_from@autentia.com";

    private static final String BASIC_SMTP_IMPOSTER_FILENAME = "SMTPImposter.json";
    
    private MountebankHandler mountebankHandler;
    
    @Before
    public void init(){
        mountebankHandler = new MountebankHandler(MOUNTEBANK_HOST);
    }

    @Test
    public void givenValidEmailthenSendEmail() throws Exception {
        
        ExampleEmailSender exampleEmailSender = new ExampleEmailSender();
        MountebankImposter imposter = null;
        try {
            imposter = mountebankHandler.parseImposterFromConfigFile(BASIC_SMTP_IMPOSTER_FILENAME);
            mountebankHandler.postImposter(imposter);
            exampleEmailSender.sendEmail(createValidTestEmail(imposter.getPort()));
            MountebankImposter imposterResponse = mountebankHandler.getImposter(imposter.getPort());

            Assert.assertNotNull(imposterResponse.getRequests());
            Assert.assertFalse(imposterResponse.getRequests().isEmpty());
            
            String emailFrom = ((Map<String,String>) (imposterResponse.getRequests().get(0).get("from"))).get("address");
            Assert.assertEquals(TEST_EMAIL_FROM, emailFrom);
        } finally{
            if (imposter != null){
                mountebankHandler.deleteImposter(imposter.getPort());
            }
        }
     }
    
    @After
    public void destroy() throws Exception{
        mountebankHandler.close();
    }
    
    private Email createValidTestEmail(int port){
        Email email = new SimpleEmail();
        email.setHostName(MOUNTEBANK_HOST);
        email.setSmtpPort(port);
        email.setSubject("Test Subject");
        try {
            email.setFrom(TEST_EMAIL_FROM);
            email.setMsg("Test Body");
            email.addTo("test_to@autentia.com");
        } catch (EmailException e) {
            Assert.fail();
        }

        return email;
    }
}

En la clase anterior se puede ver como se realizan las siguientes acciones:

  • Se instancia un MountebankHandler para el manejo del servidor de mountebank en un método de iniciación común a todos los tests.
  • Se lee la configuración del servidor de pruebas que se va a generar
  • Se crea un email de test y se intenta enviar
  • Se obtiene el valor actual del Imposter del servidor, con el objetivo de validar que se haya producido el envío del correo correctamente en el servidor
  • Se comprueba que el array de peticiones del servidor no es nulo
  • Se comprueba que el array de peticiones no esté vacío
  • Se comprueba que en la primera entrada del array de peticiones, la dirección de correo electrónico desde la que se ha enviado el correo coincide con la esperada. Se podría conseguir una solución más elegante definiendo el atributo MountebankImposter.requests como un List<MountebankRequest>, para lo cual habría que definir la clase MountebankRequest con todos los posibles valores definidos por el API, y de hecho sería la opción más recomendable en un entorno real, pero para no complicar en exceso este tutorial, se ha dejado como aparece en el código, lo que genera un warning en la linea donde se define la variable «emailFrom»
  • Finalmente, se elimina el servidor de pruebas que se ha creado y se cierra el manejador de mountebank

El fichero «SMTPImposter.json» al que se hace referencia en el código se encuentra en una carpeta incluida en el classpath de java y su contenido es el siguiente:

SMTPImposter.json
{
    "port": 4547,
    "protocol": "smtp"
}    

Además, se muestra a continuación el código de la clase MountebankHandler y MountebankImposter. La clase MountebankHandler se encarga de la creación y eliminación del servidor de pruebas dentro de mountebank, mientras que la clase MountebankImposter se trata de una clase cuyo objetivo es la representación en la aplicación de los objetos de configuración de mountebank.

MountebankHandler.java
public class MountebankHandler {


    private static final String MOUNTEBACK_SCHEME = "http";

    private static final String MOUNTEBACK_IMPOSTERS_PATH = "/imposters";
    
    private static final String MOUNTEBANK_TEST_PATH = "/test";

    private static final int MOUNTEBACK_PORT = 2525;

    private CloseableHttpClient mountebackHttpClient;

    private ObjectMapper mapper;
    
    private String mountebankHost = null;

    public MountebankHandler(String mountebankHost) {
        this.mountebankHost = mountebankHost;
        mountebackHttpClient = HttpClients.createDefault();
        mapper = new ObjectMapper();
    }

    public void deleteImposter(int port) throws Exception {
        URI uri = prepareImposterURIWithPort(port);
        HttpDelete httpDelete = new HttpDelete(uri);
        mountebackHttpClient.execute(httpDelete);
    }

    public MountebankImposter getImposter(int port) throws Exception {
        URI uri = prepareImposterURIWithPort(port);
        HttpGet httpGet = new HttpGet(uri);
        HttpResponse response = mountebackHttpClient.execute(httpGet);
        MountebankImposter imposter = parseImposterFromResponse(response);
        
        return imposter;
    }

    private MountebankImposter parseImposterFromResponse(HttpResponse response)
            throws IOException, JsonParseException, JsonMappingException {
        MountebankImposter imposter = mapper.readValue(EntityUtils.toString(response.getEntity()),
                MountebankImposter.class);
        return imposter;
    }

    private URI prepareImposterURIWithPort(int port) {
        try {
            URI uri = new URIBuilder().setScheme(MOUNTEBACK_SCHEME).setHost(mountebankHost)
                    .setPath(MOUNTEBACK_IMPOSTERS_PATH + "/" + port).setPort(MOUNTEBACK_PORT).build();
            return uri;

        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
    
    public URI prepareImposterTestURI(int port){
        try {
            URI uri = new URIBuilder().setScheme(MOUNTEBACK_SCHEME).setHost(mountebankHost)
                    .setPath(MOUNTEBANK_TEST_PATH + "/" + port).setPort(port).build();
            return uri;

        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    public void postImposter(MountebankImposter imposter) throws Exception {

        CloseableHttpResponse response = null;

        try {
            URI uri = prepareImposterURI();
            HttpEntity imposterEntity = preparePostEntity(imposter);
            HttpPost httpPost = new HttpPost(uri);
            httpPost.setEntity(imposterEntity);
            mountebackHttpClient.execute(httpPost);
        } finally {
            if (response != null) {
                response.close();
            }
        }
    }

    private HttpEntity preparePostEntity(MountebankImposter imposter)
            throws UnsupportedEncodingException, JsonProcessingException {
        StringEntity imposterEntity = new StringEntity(mapper.writeValueAsString(imposter));
        imposterEntity.setContentType("application/json");
        return imposterEntity;
    }

    public MountebankImposter parseImposterFromConfigFile(String imposterDefinitionFilename)
            throws JsonParseException, JsonMappingException, IOException {
        return this.mapper.readValue(ClassLoader.getSystemResource(imposterDefinitionFilename),
                MountebankImposter.class);
    }

    public URI prepareImposterURI() {
        try {
            URI uri = new URIBuilder().setScheme(MOUNTEBACK_SCHEME).setHost(mountebankHost)
                    .setPath(MOUNTEBACK_IMPOSTERS_PATH).setPort(MOUNTEBACK_PORT).build();
            return uri;
        } catch (URISyntaxException e) {
            throw new RuntimeException();
        }

    }

    public void close() throws IOException {
        if (mountebackHttpClient != null) {
            mountebackHttpClient.close();
        }
    }

}
MountebankHandler.java
@JsonIgnoreProperties(ignoreUnknown=true)
public class MountebankImposter {
    private String name;
    private String protocol;
    private int port;
    List<Object> stubs;
    List<Map<String,Object>> requests;

    // ... getters y setters
    
}

para ejecutar el test, lanzamos maven con el goal «test», y si todo va bien veremos que el test pasa sin problemas.

Running com.autentia.tutoriales.mounteback.ExampleEmailSenderTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.992 sec

Adicionalmente, se puede ver en la ventana de comandos donde se está ejecutando mountebank algo similar a lo siguiente:

info: [mb:2525] POST /imposters
info: [smtp:4547] Open for business...
info: [smtp:4547] 127.0.0.1 => Envelope from: test_from@autentia.com to: ["test_to@autentia.com"]
info: [mb:2525] DELETE /imposters/4547

En el log anterior se puede ver cómo mountebank ha creado un servidor SMTP en el puerto 4547, cómo se ha generado un correo electrónico y cómo se ha borrado el servidor.

6. Test de integración con un servidor HTTP doble de test

A continuación veremos como crear un test para probar un cliente HTTP utilizando un servidor doble de test creado con mountebank. En este caso la tarea va a llevarnos menos tiempo, ya que ya tenemos definidas las clases MountebankHandler y MountebankImposter.

Supongamos que tenemos una clase de negocio como la siguiente:

ExampleHttpClient.java
public class ExampleHttpClient {
    private CloseableHttpClient httpClient;
    
    public ExampleHttpClient(){
        httpClient = HttpClients.createDefault();
    }

    public String getResource(URI uri) {
        try {
            HttpGet httpGet = new HttpGet(uri);
            CloseableHttpResponse response = httpClient.execute(httpGet);
            String resource = EntityUtils.toString(response.getEntity());
            return resource;
            
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

El test que tenemos que definir para dicho cliente asume que el recurso que se va a obtener es siempre la cadena «OK» en formato texto plano ante peticiones GET. Para probar dicho cliente empezamos definiendo el fichero de configuración del servidor, que quedaría de la siguiente forma:

{
    "name": "HTTPImposter",
    "protocol": "http",
    "port": 8484,
    "stubs": [
    {
        "responses": [
        {
            "is": {
                "statusCode": 200,
                "body": "OK"
            }
        }
        ]
    }
    ]
}

Esta configuración define un «stub» que hace que el servidor responda usando el protocolo HTTP sobre el puerto 8484. La respuesta siempre será «OK», con código de estatus 200. Si por ejemplo quisiéramos testear un cliente que crea objetos en el servidor mediante el método POST, tendríamos que simular que en la primera petición el objeto se haya creado (devolviendo estatus 201) y devolviendo error en las siguientes peticiones sobre el mismo recurso puesto que se supone que ya existiría. Para ello, bastaría con añadir más objetos dentro del array «responses», teniendo en cuenta que las respuestas se irán entregando en el mismo orden en el que aparezcan definidas en el objeto JSON. Además, se podrían incluir encabezados en la respuesta tales como «Content-Type» o «Location».

Una vez definido la configuración del servidor de pruebas, modificamos la clase de test para incorporar la lógica de arranque y parada del servidor de pruebas de Mountebank, quedando el código de la siguiente forma:

ExampleHttpClientTest.java
public class ExampleHttpClientTest {

    private static final String BASIC_SMTP_IMPOSTER_FILENAME = "HTTPImposter.json";
    private static final String MOUNTEBANK_HOST = "localhost";

    private MountebankHandler mountebankHandler;
    
    @Before
    public void init() throws Exception{
        mountebankHandler = new MountebankHandler(MOUNTEBANK_HOST);
    }
    
    @Test
    public void givenValidURIthenGetResource() throws Exception{
        
        MountebankImposter imposter = null;
        MountebankImposter imposterResponse = null;
         try {
             imposter = mountebankHandler.parseImposterFromConfigFile(BASIC_SMTP_IMPOSTER_FILENAME);
             mountebankHandler.postImposter(imposter);

             ExampleHttpClient exampleHttpClient = new ExampleHttpClient();
             URI httpUri = mountebankHandler.prepareImposterTestURI(imposter.getPort());
             Assert.assertEquals("OK", exampleHttpClient.getResource(httpUri));

             imposterResponse = mountebankHandler.getImposter(imposter.getPort());

             Assert.assertNotNull(imposterResponse.getRequests());
             Assert.assertFalse(imposterResponse.getRequests().isEmpty());
             
        } finally{
            if (imposterResponse != null){
               mountebankHandler.deleteImposter(imposterResponse.getPort());
            }
        }
        
    }
    
    @After
    public void destroy() throws Exception{
        mountebankHandler.close();
    }
}

Una vez tenemos todo listo, ejecutamos el goal «test» de maven y el resultado debería ser similar al siguiente:

Running com.autentia.tutoriales.mounteback.ExampleEmailSenderTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.591 sec
Running com.autentia.tutoriales.mounteback.ExampleHttpClientTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.017 sec

Results :

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0    

7. Conclusiones

En este tutorial hemos visto como instalar y configurar la herramienta Mountebank para crear servidores de pruebas para nuestros tests. Para entender mejor su funcionamiento hemos realizado una serie de ejemplos en Java utilizando Maven. El primer ejemplo presenta una configuración básica basada en el envío de un correo electrónico y posteriormente un ejemplo algo más complejo probando un cliente HTTP.

8. Referencias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad