Curso Jakarta EE 9 (11). Docker (4): Pruebas con Arquillian Cube.

logo Jakarta EE

En el capítulo anterior conseguimos ejecutar las pruebas automáticas en un contenedor de una imagen de WildFly creada a medida de nuestras necesidades, aunque puede parecer que no hemos avanzado mucho: seguimos teniendo que arrancar el servidor manualmente. Vamos a remediar esto.

>>>> ÍNDICE <<<<

Arquillian Cube

Vamos a hacer que Arquillian se encargue de todo con una configuración equivalente a la del modo managed para que gestione el ciclo de vida de los contenedores necesarios para las pruebas que, de momento, solo es uno. El objetivo final es conseguir que nuestras pruebas automáticas sean lo más autónomas posibles y puedan ejecutarse en cualquier máquina que tenga Java y Docker pulsando un botón (más o menos).

La funcionalidad que queremos la entrega Arquillian Cube. Partiendo de un fichero docker-compose, o un formato de configuración propio pero similar, esta extensión de Arquillian puede crear desde el cero el entorno de pruebas -y esto incluye construir imágenes-, desplegarlo y, cuando Arquillian haya terminado de ejecutar las pruebas, destruirlo sin dejar rastro, eliminando las redes, volúmenes, contenedores e imágenes creadas. Este funcionamiento se muestra en la siguiente figura.

Configuración

Empecemos añadiendo estos «superpoderes» al proyecto arquillian con un nuevo perfil en el pom.xml. Incluye la dependencia para Arquillian Cube y, atención, el adaptador de WildFly en remoto, responsable de lanzar las pruebas en cuanto Arquillian Cube informe que ha terminado el arranque de los contenedores.

<profile>
    <id>arq-wildfly-docker</id>
    <dependencies>
        <dependency>
            <groupId>org.arquillian.cube</groupId>
            <artifactId>arquillian-cube-docker</artifactId>
            <version>${arquillian.cube.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.wildfly.arquillian</groupId>
            <artifactId>wildfly-arquillian-container-remote</artifactId>
            <version>${arquillian.wildfly.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- Arquillian cube on Java 11-->
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>javax.activation-api</artifactId>
            <version>1.2.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${maven.surefire.version}</version>
                <configuration>
                    <systemPropertyVariables>
                        <arquillian.launch>wildfly_docker</arquillian.launch>
                    </systemPropertyVariables>
                </configuration>
            </plugin>
        </plugins>
    </build>
</profile>

Ahora, la aplicación de ejemplo cuenta con cuatro perfiles.

Arquillian Maven Profiles 4

En la carpeta /src/test/resources/docker ubicamos el Dockerfile del capítulo anterior y un fichero docker-compose.yml con el siguiente contenido.

version: '3'
services:
  arquillian-cube-wildfly:
    build:
        context: src/test/resources/docker
    ports:
      - "8989:8080"
      - "10090:9990"

No hay demasiado que comentar. De hecho, la configuración es más simple que la que vimos en su momento. Se define un servicio basado en una imagen dada por un Dockerfile (por eso se ha usado build en lugar de image) con sus puertos. Arquillian Cube construye la imagen y el correspondiente contenedor con el nombre del servicio. El contexto debe ser relativo a la carpeta raíz del proyecto (donde está el pom).

En el fichero arquillian.xml hacemos una configuración de tipo remoto con el nombre que pusimos en el perfil wildfly_docker. Aunque el contenedor se ejecute en nuestra máquina y accedamos a WildFly con la url localhost, motivo por el cual no es necesario configurar las propiedades host y managementAddress, en realidad nos conectamos a una máquina remota. Recordemos que este escenario nos exige indicar las credenciales de un usuario administrador. Asimismo, al menos en Windows, suele ser necesario configurar un timeout generoso.

<container qualifier="wildfly_docker">
    <configuration>
        <property name="managementPort">10090</property>
        <property name="username">admin</property>
        <property name="password">admin</property>
        <property name="port">8989</property>
        <property name="connectionTimeout">20000</property>
    </configuration>
</container>

Y no se necesita nada más… en Linux. En Windows, si ejecutamos las pruebas con el perfil arq-wildfly-docker probablemente se produzca este error.

com.github.dockerjava.api.exception.DockerClientException: Unsupported protocol scheme found: 'npipe:////./pipe/docker_engine'. Only 'tcp://' or 'unix://' supported.

Docker en Windows utiliza el protocolo npipe para la comunicación con Docker Engine y la librería docker-java, usada por Arquillian Cube, no lo soporta. Tendremos que habilitar el acceso por tcp -es inseguro pero no hay otra opción- en Docker Desktop.

Así indicamos en arquillian.xml la cadena de conexión a Docker Engine.

<extension qualifier="docker">
    <property name="serverUri">tcp://localhost:2375</property>
</extension>

Si no se especifica la propiedad serverUri pero se ha declarado la variable de sistema DOCKER_HOST, se usará su valor.

Con todo, esta problemática plantea un reto si las pruebas son susceptibles de ser ejecutadas tanto en Windows en Linux. Un caso muy típico en el mundo empresarial consiste en desarrollar con el primero y tener un servidor de integración continua (Jenkins y similares) bajo el segundo. La solución puede ser tan sencilla como crear la variable DOCKER_HOST, pero insisto otra vez en conseguir que las pruebas se puedan lanzar en cualquier máquina realizando con la configuración mínima posible.

De nuevo, recurrimos a los perfiles de Maven y a la posibilidad de activarlos de forma automática según ciertas condiciones entre las que se incluyen el sistema operativo. La idea es establecer DOCKER_HOST con el valor adecuado para Windows, el entorno donde tenemos el problema.

        <profile>
            <id>test-windows-docker-url</id>
            <activation>
                <os>
                    <family>windows</family>
                </os>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-surefire-plugin</artifactId>
                        <version>${maven.surefire.version}</version>
                        <configuration>
                            <systemPropertyVariables>
                                <DOCKER_HOST>tcp://localhost:2375</DOCKER_HOST>
                            </systemPropertyVariables>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>

El perfil test-windows-docker-url se activa cuando ejecutamos Maven en Windows y configura el plugin surefire para que en las pruebas tengamos disponible la propiedad del sistema DOCKER_HOST con la url que usará Arquillian Cube. La configuración que aquí se hace del plugin se combina sin problemas con la realizada en cada perfil especifico (arq-wildfly-docker). Como «daño colateral», ahora la opción activeByDefault que pusimos en su momento en el perfil arq-wildfly-managed ya no tiene sentido, pues nunca se aplicará cuando se active test-windows-docker-url. Esto nos obliga a especificar siempre el perfil que queremos.

Ya sea en Windows o Linux, todas las pruebas deberían ejecutarse con éxito. Eso sí, se requiere paciencia porque hay que construir la imagen.

Jan 24, 2021 9:17:13 PM org.arquillian.cube.docker.impl.client.CubeDockerConfigurationResolver resolveSystemDefaultSetup
INFO: Connected to docker (saori) using default settings version: 20.10.2 kernel: 5.4.0-62-generic
Jan 24, 2021 9:17:13 PM org.arquillian.spacelift.Spacelift$SpaceliftInstance 
INFO: Initialized Spacelift from defaults, workspace: /opt/CURSO/CODE/test-arquillian, cache: /home/dani/.spacelift/cache
CubeDockerConfiguration: 
  serverUri = unix:///var/run/docker.sock
  tlsVerify = false
  dockerServerIp = localhost
  definitionFormat = COMPOSE
  clean = false
  removeVolumes = true
  dockerContainers = containers:
  arquillian-cube-wildfly:
    alwaysPull: false
    buildImage: {dockerfileLocation: src/test/resources/docker, noCache: true, remove: true}
    killContainer: false
    manual: false
    networkMode: resources_default
    networks: [resources_default]
    portBindings: !!set {10090->9990/tcp: null, 8989->8080/tcp: null}
    readonlyRootfs: false
    removeVolumes: true
networks:
  resources_default: {driver: bridge}


[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 50.073 s - in com.danielme.jakartaee.arquillian.HelloServletArquillianTest
[INFO] Running com.danielme.jakartaee.arquillian.MessageArquillianTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.339 s - in com.danielme.jakartaee.arquillian.MessageArquillianTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:00 min
[INFO] Finished at: 2021-01-24T21:18:10+01:00
[INFO] ------------------------------------------------------------------------

Un par de observaciones importantes sobre la configuración que estamos usando.

  • Arquillian requiere que el fichero Dockerfile se llame, valga la redundancia, Dockerfile, incluso definiendo el nombre en el docker-compose con la opción dockerfile. Si tenemos varios, se ubicarán en directorios distintos y configuraremos en cada servicio su propiedad context.
  • El fichero docker-compose no se puede ejecutar con Docker Compose debido a que el contexto src/test/resources/docker solo es válido para Arquillian Cube, quien lo aplica desde la raíz del proyecto para encontrar el Dockerfile. Esto se puede solucionar, además de utilizando imágenes ya construidas, poniendo docker-compose.yml en la raíz del proyecto.

A veces, si Arquillian Cube falla no elimina la red interna de los contenedores y luego no podremos ejecutar las pruebas por este error.

Caused by: com.github.dockerjava.api.exception.BadRequestException: {"message":"network resources_default is ambiguous (2 matches found on name)"}

Cuando surja este problema, borraremos las redes con el nombre que indica el mensaje de error (resources_default).

$ docker network ls
NETWORK ID     NAME                DRIVER    SCOPE
249e1290f8ad   bridge              bridge    local
177c04bda66f   host                host      local
2ae0505b0d73   none                null      local
dde9de085e79   resources_default   bridge    local
d1a1bb896b58   resources_default   bridge    local
$ docker network rm dde9de085e79 d1a1bb896b58

También podemos eliminar todas las que no estén en uso.

$ docker network prune

De la correcta ejecución de las pruebas se deduce que Arquillian Cube es capaz de encontrar el fichero docker-compose dentro del proyecto. Para configuraciones complejas en las que tengamos más de un docker-compose y/o se encuentren en una URL, podemos indicar sus ubicaciones, separadas por «,», creando la siguiente configuración.

<extension qualifier="docker">
    <property name="dockerContainersFiles">
         src/test/resources/docker/docker-compose.yml
     </property>
    <!-- Only for Windows -->
    <!--<property name="serverUri">tcp://localhost:2375</property>-->
</extension>

Cada vez que lanzamos un conjunto de pruebas, Arquillian Cube genera desde cero los contenedores y asegura una ejecución limpia. En nuestro caso, esta acción implica construir siempre la imagen a partir del Dockerfile incluyendo todas sus capas. Esto no es nada rápido y supone un gran problema cuando estamos programando y lanzamos cada dos por tres las pruebas.

La solución evidente es construir la imagen y usarla en el docker-compose.

$ docker image build -t personalBudget/wildfly-arquillian .
version: '3'
services:
  arquillian-cube-wildfly:
    image: personalBudget/wildfly-arquillian
    ports:
      - "8989:8080"
      - "10090:9990"

Pese a que funciona, exige un compromiso: los cambios en el Dockerfile requieren generar la imagen de forma manual. Por consiguiente, las pruebas serán menos autónomas. En equipos de desarrollo grandes, lo recomendable es distribuir la última versión de la imagen mediante un repositorio como JFrog Artifactory o el mismo Docker Hub.

El formato Cube

La buena noticia para solucionar el problema anterior es que el comportamiento de Arquillian Cube es muy configurable, y una de sus numerosas opciones permite utilizar la caché de Docker para construir la imagen desde el Dockerfile. En realidad, lo que sucede es que de forma predeterminada la caché está desactivada, tal y como podemos comprobar en la traza que puse anteriormente.

 buildImage: {dockerfileLocation: src/test/resources/docker, noCache: true, remove: true}

Pero también hay una mala noticia: se deben configurar los contenedores con YAML en un formato propio de Arquillian Cube (*). Tenemos que renunciar a nuestro docker-compose.yml, aunque la traducción entre ambos mundos es sencilla.

(*) Algunas de las propiedades de Arquillian se pueden definir con cubeSpecificProperties, lo que permite seguir utilizando el docker-compose.yml.

     <extension qualifier="docker">
        <property name="definitionFormat">CUBE</property>
        <property name="autoStartContainers">arquillian-cube-wildfly</property>
        <property name="dockerContainers">
        arquillian-cube-wildfly:
            buildImage:
                dockerfileLocation: src/test/resources/docker
                noCache: false
            portBindings: [8989->8080/tcp, 10090->9990/tcp, 8787/tcp]
        </property>
        <!-- Only for Windows -->
        <!--<property name="serverUri">tcp://localhost:2375</property>-->
    </extension>

Analicemos la configuración anterior.

  • El formato a usar es CUBE. La otra opción es COMPOSE.
  • Queremos arrancar el servicio arquillian-cube-wildfly.
  • Hemos trasladado el contenido de docker-compose.yml a la propiedad dockerContainers. Con buildImage se indica que el contenedor debe crearse a partir de una imagen definida en un Dockerfile. Arquillian Cube la construirá reutilizando las capas que ya hayan sido generadas porque noCache es falso. También exponemos los puertos que necesitemos.

Si ejecutamos de nuevo las pruebas, la diferencia en la velocidad resulta abismal. La creación de la imagen es pesada por la descarga de WildFly y la JDK, tiempo que ahora nos ahorramos tras haber ejecutado las pruebas una primera vez. Obsérvese que la imagen se sigue construyendo siempre, así que los que cambios en el Dockerfile se aplican enseguida sin ninguna intervención por nuestra parte.

Son muchas las posibilidades de personalización que ofrece el formato Cube y examinarlas queda fuera del alcance de este curso. Cuando tengamos que escribir pruebas que requieran tanto de WildFly como de MySQL, veremos ejemplos más complejos con nuevos parámetros.

Mejorar el rendimiento

Si bien es cierto que hemos conseguido que las pruebas sean más rápidas, todavía hay que arrancar los contenedores y esperar al inicio de WildFly en cada tanda de ejecución. La alternativa para evitar las dos esperas (la construcción del contenedor y el arranque de WildFly) consiste en cambiar el modo de integración de Arquillian Cube con Docker usando la propiedad connectionMode. Tenemos tres opciones.

  • STARTANDSTOP. Es el comportamiento predeterminado. Como hemos comprobado (y padecido), arranca y destruye los contenedores. Es precisamente lo que no nos conviene.
  • STARTORCONNECT. Si el contenedor ya está en ejecución, se utiliza. Si no, se arranca y detiene tal y como se hace en el modo STARTANDSTOP.
  • STARTORCONNECTANDLEAVE. Igual que STARTORCONNECTANDLEAVE, pero si hay que iniciar el contenedor no se detendrá cuando termine la ejecución de las pruebas.

La opción ganadora es la tercera. Con ella conseguimos que la primera vez que se lancen los tests se inicie todo el entorno de contenedores, quedando disponible para las próximas ejecuciones.

<extension qualifier="cube">
    <property name="connectionMode">STARTORCONNECTANDLEAVE</property>
</extension>

Téngase en cuenta que con esta configuración cuando ya no necesitemos los contenedores debemos destruirlos todos, incluyendo su red interna. De lo contrario, Arquillian Cube no podrá hacer un nuevo arranque de ellos porque ya existirán contenedores con los mismos nombres. Es lo que informa este error.

java.lang.RuntimeException: Could not auto start container arquillian-cube-wildfly
Caused by: org.arquillian.cube.spi.CubeControlException: Could not create arquillian-cube-wildfly
Caused by: com.github.dockerjava.api.exception.ConflictException: 
{"message":"Conflict. The container name "/arquillian-cube-wildfly" is already in use by container "26d58509d061abf34a9b9dab302a0799f3c7d50a80a0f928ceb490052bbb8137". You have to remove (or rename) that container to be able to reuse that name."}

Código de ejemplo

El código de ejemplo para este capítulo se encuentra en GitHub (todos los proyectos están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.

>>>> ÍNDICE <<<<

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.