Curso Jakarta EE 9 (7). Pruebas automáticas (3): Testing en WildFly con Arquillian.

logo Jakarta EE

Para realizar pruebas unitarias, tenemos suficiente con JUnit 5 y AssertJ. Pero cuando llegue el momento de los test de integración y end-to-end, necesitaremos usar los servicios de WildFly, o bien disponer de la aplicación desplegada en el servidor para la ejecución de pruebas que simulen clientes remotos.

>>>> ÍNDICE <<<<

Presentando Arquillian

Arquillian es un marco de trabajo orientado a la realización de pruebas, no necesariamente en aplicaciones Jakarta EE, que requieran un servidor (WildFly, Tomcat, GlassFish…). Se integra, entre otros, con JUnit y Maven, y reduce el «dolor» que supone implementar pruebas de integración y e-2-e.

De forma muy simplificada, y dependiendo de la configuración, puede realizar lo siguiente.

  1. Arrancar el servidor.
  2. Construir y desplegar un artefacto con el código de la aplicación que será el sujeto de las pruebas.
  3. Ejecutar las pruebas, en nuestro caso tests de Jupiter (JUnit 5), dentro del artefacto desplegado o de forma externa «contra» la aplicación que contiene (en general, con llamadas HTTP).
  4. Replegar el artefacto y detener el servidor.

Esta imagen ilustra de forma esquemática esta secuencia de acciones.

La integración con los servidores se realiza mediante tres tipos de «adaptadores».

  • Remoto. El servidor está operativo y accesible. Arquillian ni lo arranca ni lo detiene e interactúa con él utilizando protocolos remotos. Aquí el concepto «remoto» no debe llevarnos a confusión: el servidor puede estar ubicado en la misma máquina desde la que se lanzan las pruebas.
  • Gestionado (managed). Igual que remoto, pero Arquillian controla el ciclo de vida del servidor, es decir, lo arranca y detiene según sea necesario.
  • Embebido. Modo gestionado en el que el servidor se ejecuta en la misma jvm que las pruebas (Maven o el IDE) por lo que la comunicación se realiza de forma local.

La disponibilidad de estos modos depende del servidor. WildFly cuenta con adaptadores para los tres y los examinaremos de forma práctica en este capítulo. Más adelante, examinaremos el uso de Docker.

Integrando Arquillian, JUnit 5 y WildFly

Si bien Arquillian es fácil de utilizar a nivel básico -otro tema es, por ejemplo, el testeo de microservicios-, requiere de una configuración algo laboriosa. Tanto Eclipse como IntelliJ cuentan con plugins, pero en el curso recurriremos a Maven para que los proyectos sean independientes de cualquier IDE. No obstante, veremos cómo ejecutar y depurar las pruebas basadas en Arquillian en estos IDE utilizando configuraciones genéricas.

Configuremos paso a paso la integración de Arquillian con WildFly y JUnit 5 en Maven. Nuestro primer objetivo es conseguir que Arquillian arranque y detenga un servidor WildFly para ejecutar las pruebas. Estarán incluidas en un artefacto desplegado en el servidor, tal y como mostraba la ilustración de la sección anterior, de tal modo que puedan hacer uso de sus servicios (fuentes de datos, inyección de dependencias…).

Lo primero es añadir al pom del proyecto de ejemplo, llamado arquillian, las dependencias. De acuerdo al capítulo anterior, recomiendo asegurar que se utilice la última versión de maven-surefire-plugin. No usaremos el plugin failsafe.

 <plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-surefire-plugin</artifactId>
     <version>${maven.surefire.version}</version>
 </plugin>

JUnit 5 con Jupiter.

 <dependency>
     <groupId>org.junit.jupiter</groupId>
     <artifactId>junit-jupiter-api</artifactId>
     <version>${junit.api.version}</version>
     <scope>test</scope>
 </dependency>

La integración de Arquillian con JUnit 5.

<dependency>
    <groupId>org.jboss.arquillian.junit5</groupId>
    <artifactId>arquillian-junit5-container</artifactId>
    <version>${arquillian.version}</version>
    <scope>test</scope>
</dependency>

El módulo para usar WildFly en modo gestionado.

<dependency>
     <groupId>org.jboss.arquillian.protocol</groupId>
     <artifactId>arquillian-protocol-servlet-jakarta</artifactId>
     <version>${arquillian.version}</version>
     <scope>test</scope>
</dependency>
 <dependency>
     <groupId>org.wildfly.arquillian</groupId>
     <artifactId>wildfly-arquillian-container-managed</artifactId>
     <version>${arquillian.wildfly.version}</version>
     <scope>test</scope>
 </dependency>

Seguiremos escribiendo las aserciones con AssertJ. Importamos dos dependencias, más adelante descubriremos el motivo.

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>${assertj.core.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
    <version>${shrinkwrap.resolver.version}</version>
    <scope>test</scope>
 </dependency>

En el fichero /src/test/resources/arquillian.xml vamos a configurar WildFly.

<arquillian
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://jboss.org/schema/arquillian"
	xsi:schemaLocation="http://jboss.org/schema/arquillian
	http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

	<defaultProtocol type="Servlet 5.0"/>

	<container qualifier="wildfly" default="true">
	    <configuration>
	        <property name="jbossHome">/opt/wildfly-preview-22.0.0.test</property>
	        <property name="javaVmArguments">-Djboss.socket.binding.port-offset=100</property>
	        <property name="managementPort">10090</property>
	    </configuration>
	</container>

</arquillian>

En defaultProtocol se indica el protocolo que Arquillian utilizará para solicitar la ejecución de las pruebas en el servidor y recibir los resultados. La versión 5.0 de la especificación Jakarta Servlet es la incluida en Jakarta EE 9. En la práctica, esto se traduce en que el artefacto contendrá un servlet que permite gestionar las pruebas. Todo este funcionamiento es interno y automático.

La propiedad jbossHome no requiere de muchas explicaciones. Es la ruta absoluta a la carpeta de WildFly. Recomiendo tener una instalación dedicada a las pruebas automáticas y no usarla para el desarrollo. De este modo evitaremos problemas de artefactos duplicados.

El resto de propiedades son opcionales. javaVmArguments contiene una cadena que se agrega al comando que arranca WildFly. Usamos el parámetro jboss.socket.binding.port-offset para cambiar, sumando un número, todos los puertos con el objetivo de evitar que la instancia de WildFly utilizada para las pruebas no pueda iniciarse porque ya exista otro servicio en ejecución que haga uso de alguno de los puertos predeterminados. Así, el puerto del servidor HTTP 8080 pasa a ser 8180 (8080 + 100).

Por consiguiente, también hay que utilizar la propiedad managementPort para definir el puerto en el que escucha la API de administración (en el ejemplo es el predeterminado (9990) más 100) con la que Arquillian desplegará el artefacto. Puesto que el servidor está en nuestra máquina local, no es obligatorio habilitar en WildFly un usuario administrador.

La primera prueba

Ya tenemos toda la infraestructura configurada y estamos en condiciones de empezar a implementar las pruebas. Ahora veremos en acción el sistema de extensiones de JUnit 5: Arquillian lo utiliza para interactuar con WildFly en los momentos adecuados (puntos de extensión) del ciclo de ejecución de las pruebas. La clase con los tests debe usar la extensión ArquillianExtension. También necesitamos un método estático anotado con @Deployment que devuelva el artefacto de tipo war a desplegar..

Esta clase cumple los dos requisitos anteriores.

package com.danielme.jakartaee.arquillian;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
import org.jboss.shrinkwrap.resolver.api.maven.ScopeType;
import org.junit.jupiter.api.extension.ExtendWith;

import java.io.File;

@ExtendWith(ArquillianExtension.class)
class MessageArquillianTest {

    @Deployment
    public static WebArchive createDeployment() {
        File[] assertjFiles = Maven.resolver()
                   .loadPomFromFile("pom.xml")
                   .resolve("org.assertj:assertj-core")
                   .withTransitivity()
                   .asFile();
       return ShrinkWrap.create(WebArchive.class, "arquillian-test.war")
                   .addClass(Message.class)
                   .addAsLibraries(assertjFiles);
    }

}

El artefacto, llamado arquillian-test.war, se construye con la API fluida de ShrinkWrap y solo debe contener las clases y recursos imprescindibles. Su inclusión se hace concatenando llamadas a los métodos que empiezan por add (addClasses, addPackage, addPackages, addAsWebInfResource…). Solo necesitaremos la clase Message que veremos en breve. De momento, es suficiente con este ejemplo tan sencillo; a medida que el curso avance, iremos viendo la construcción de artefactos con más elementos, y aquí me limitaré a nombrar los métodos más básicos.

  • addClass. Añade una clase individual. Es lo que haremos en los ejemplos de este capítulo.
  • addClasses. Añade un conjunto de clases.
addClasses(Message.class, HelloServlet.class)
  • addPackage. Añade todo el paquete en el que se encuentra una clase.
.addPackage(Message.class.getPackage())
.addPackage("com.danielme.jakartaee.arquillian")
  • addPackages. Añade una lista de paquetes, con la posibilidad de incluir de forma recursiva los paquetes hijos con el primer parámetro de tipo lógico.
.addPackages(true, Message.class.getPackage())
.addPackages(false, Message.class.getPackage(), Service.class.getPackage())
.addPackages(true, "com.danielme.jakartaee.arquillian")

La prueba que vamos a escribir estará dentro de arquillian-test.war y utiliza AssertJ. Esto supone una dificultad: las dependencias de tipo test jamás se empaquetan, con la inevitable excepción de las que necesita Arquillian. Por tanto, hay que añadir la librería al artefacto, y esta es la causa de que el pom contenga la dependencia shrinkwrap-resolver-impl-maven. Se ha utilizado la versión declarada en el pom, aunque también es posible añadir de forma explícita una concreta.

.addAsLibraries(Maven.resolver()
.resolve("org.assertj:assertj-core:3.18.1")
.withTransitivity().asFile());

Una alternativa más radical: incluir todas las dependencias.

File[] files = Maven.resolver()
                .loadPomFromFile("pom.xml")
                .importRuntimeAndTestDependencies()
                .resolve()
                .withTransitivity()
                .asFile();

Prosigamos con la creación de nuestro test. En MessageArquillianTest probaremos la siguiente clase que tiene un método que devuelve una cadena.

package com.danielme.jakartaee.arquillian;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class Message {

    private static final String HELLO_MESSAGE = "Jakarta EE rocks!!";

    public String get() {
        return HELLO_MESSAGE;
    }

}

Me adelanto a los capítulos dedicados a la especificación Jakarta CDI; hasta entonces, es suficiente saber que la anotación @ApplicationScoped indica que la aplicación tendrá una única instancia de Message y que será proporcionada de forma automática a todos los objetos que lo soliciten mediante la anotación @Inject. Todo esto es responsabilidad del servidor de aplicaciones.

Escribamos en MessageArquillianTest una prueba unitaria. Esta clase forma parte de arquillian-test.war y puede recibir la (única) instancia de Message la cual es creada y gestionada por el servidor.

@Inject
private Message service; 

@Test
@DisplayName("probando inyección de Message")
void testMessageBean() {
     assertThat(service.get()).isEqualTo(HELLO_MESSAGE);
 }

testMessageBean se ejecuta como cualquier prueba de JUnit 5 sin ninguna particularidad especial por el hecho de utilizar Arquillian. Por tanto, es posible lanzarla con Maven, IntelliJ o Eclipse. A este último pertenece la siguiente captura de pantalla.

eclipse test junit 5 arquillian wildlfy

¡Pura magia! Como parte de la ejecución de los tests, Arquillian arrancará WildFly, desplegará arquillian-test.war y detendrá el servidor cuando todas las pruebas finalicen, eliminando el artefacto.

Curioseando en la carpeta temporal de WildFly podemos comprobar los elementos desplegados.

arquillian artifact deployed in WildFly

A veces el despliegue falla porque el artefacto ya está en el servidor. Esto puede pasar si por algún problema WildFly no se detuvo correctamente en una ejecución previa (Arquillian debería eliminar sus despliegues al finalizar las pruebas) o si lanzamos las pruebas con un IDE y luego lo intentamos hacer desde una terminal con Maven. El error es el que sigue.

"WFLYCTL0212: Duplicate resource [(\"deployment\" => \"arquillian-test.war\")]"}}

Ante esta situación, procederemos a eliminar el contenido de la carpeta {wildfly}/standalone/tmp.

WildFly en modo remoto y perfiles Maven

Revisando la imagen que muestra el resultado de la prueba, se hace patente uno de los retos del testing de integración: el arranque del servidor y el despliegue de la aplicación supone un lastre importante en los tiempos de ejecución, varios órdenes de magnitud en comparación con las pruebas unitarias.

La estrategia que hemos seguido (servidor «managed» gestionado por Arquillian), y dejando al margen el uso de Docker, es práctica si pretendemos que la ejecución de las pruebas sea lo más autónoma posible. Sin embargo, si estamos desarrollando y las lanzamos con frecuencia, nos interesa reducir los tiempos utilizando WildFly en modo remoto. Con este modo, recordemos, el servidor ya está operativo y Arquillian procederá a desplegar y replegar su artefacto. Nos ahorramos los tiempos de arranque del servidor, pero debemos iniciarlo y detenerlo nosotros mismos.

La configuración del modo remoto la haremos de forma que pueda convivir con el gestionado que ya tenemos para que podamos elegir cómo queremos utilizar WildFly según nos convenga en cada momento. La solución la aportan los perfiles de Maven al permitir la definición de configuraciones seleccionables. Se puede encontrar más información al respecto en este pequeño tutorial.

Eliminemos del pom el plugin surefire y la dependencia wildfly-arquillian-container-managed. Creamos un perfil para cada modo en los que definimos esos dos elementos: el plugin surefire, para poder indicar con su propiedad arquillian.launch la configuración (el valor qualifier) a utilizar de entre las disponibles en el fichero arquillian.xml, y el adaptador de WildFly.

<profiles>

    <profile>
        <id>arq-wildfly-managed</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>${maven.surefire.version}</version>
                    <configuration>
                        <systemPropertyVariables>
                            <arquillian.launch>wildfly_managed</arquillian.launch>
                        </systemPropertyVariables>
                    </configuration>
                </plugin>
            </plugins>
        </build>
        <dependencies>
            <dependency>
                <groupId>org.wildfly.arquillian</groupId>
                <artifactId>wildfly-arquillian-container-managed</artifactId>
                <version>${arquillian.wildlfy.version}</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    </profile>

    <profile>
        <id>arq-wildfly-remote</id>
        <dependencies>
            <dependency>
                <groupId>org.wildfly.arquillian</groupId>
                <artifactId>wildfly-arquillian-container-remote</artifactId>
                <version>${arquillian.wildlfy.version}</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_remote</arquillian.launch>
                        </systemPropertyVariables>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>

</profiles>

Aunque no vamos a hacerlo, merece la pena indicar que el plugin surefire permite el filtrado de las pruebas a ejecutar. Por ejemplo.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M5</version>
    <configuration>
        <includes>
            <include>**/integration/**</include>
       </includes>
    </configuration>
</plugin>

Esta configuración sólo ejecutará las pruebas contenidas en los paquetes integration. Para más información, consultar la documentación.

Como podemos apreciar, cada perfil hace referencia a una configuración específica de arquillian.xml. La siguiente imagen muestra esa relación.

Arquillian Maven Profiles

En wildfly_remote podemos usar los parámetros de esta tabla.

ParámetroPor omisiónDescripción
host127.0.0.1La url en la que se encuentra el servidor.
port8080El puerto del servidor web de WildFly.
username Usuario administrador.
password Contraseña del usuario administrador.
managementAddress127.0.0.1La url de la consola\API de administración.
managementPort9990El puerto consola\API de administración.
connectionTimeout5000El tiempo de espera en milisegundos para las llamadas de Arquillian a WildFly.

Si WildFly se está ejecutando en local con los puertos preestablecidos, no hay que configurar nada. Es más, ni siquiera se requiere un usuario administrador. La configuración puede estar vacía tal y como se muestra a continuación.

	<container qualifier="wildfly_remote">
	</container>

Si el servidor está en otra máquina, hay que crear un usuario administrador y proporcionar sus credenciales, además de la url. Asimismo, WildFly debe aceptar conexiones remotas, esto es, realizadas desde fuera de la máquina en la que se está ejecutando, ya que por seguridad solo admite conexiones locales. Esto se puede hacer utilizando los siguientes parámetros en el momento de iniciar el servidor con el script de arranque.

(LINUX)   ./standalone.sh -b 0.0.0.0 -bmanagement 0.0.0.0
(WINDOWS) standalone.bat -b 0.0.0.0 -bmanagement 0.0.0.0

Con estas dos opciones estamos especificando las direcciones ip desde las que se admiten conexiones para la interfaz web estándar y la de administración, que serán todas si usamos «0.0.0.0». Son una abreviatura de -Djboss.bind.address y -Djboss.bind.address.management. La configuración por omisión la encontramos en el fichero standalone.xml de WildFly.

<interfaces>
	<interface name="management">
		<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
	</interface>
	<interface name="public">
		<inet-address value="${jboss.bind.address:127.0.0.1}"/>
	</interface>
</interfaces>

Si arrancamos el servidor con el siguiente comando en una máquina que tiene por dirección IP «192.168.1.129»…

(LINUX)   ./standalone.sh -b 0.0.0.0 -bmanagement 0.0.0.0 -Djboss.socket.binding.port-offset=100
(WINDOWS) standalone.bat -b 0.0.0.0 -bmanagement 0.0.0.0 -Djboss.socket.binding.port-offset=100

…usaremos esta configuración, suponiendo que existe un usuario administrador con las credenciales admin/admin.

<container qualifier="wildfly_remote">
	<configuration>
	    <!-- web -->
		<property name="host">192.168.1.129</property>
		<property name="port">8180</property>
		<!-- administration -->
		<property name="managementAddress">192.168.1.129</property>
		<property name="managementPort">10090</property>
		<property name="username">admin</property>
		<property name="password">admin</property>

		<!-- time out, default: 5000-->
		<!--<property name="connectionTimeout">10000</property>-->
	</configuration>
</container>

Consejo: evitaremos muchos dolores de cabeza comprobando con un navegador que tenemos acceso a la consola de administración y a la página de inicio de WildFly con los parámetros definidos en arquillian.xml.

El perfil arq-wildfly-managed es el predeterminado (tiene el flag activeByDefault). Si ejecutamos las pruebas como hasta ahora nada cambiará, y Arquillian arrancará y detendrá el servidor. Para elegir un perfil, tenemos la opción -P.

$ ./mvnw clean test -Parq-wildfly-remote

La instrucción anterior, basada en el wrapper de Maven que incluyo en todos los proyectos del curso, ejecuta las pruebas en el WildFly remoto.

En IntelliJ, la vista de Maven permite seleccionar el perfil a utilizar en el proyecto. Si no aparece, la activamos en View->Tools Windows. A veces, al variar el perfil el cambio no se aplica y hay que pulsar el icono de refrescar.

IntelliJ Maven profiles

Accedemos a la funcionalidad equivalente en Eclipse seleccionando Maven->Select Maven Profiles… en el menú contextual del proyecto, o bien con el atajo Control+Alt+P.

Eclipse perfiles Maven

Lastimosamente, Eclipse ignora la configuración del plugin surefire y la propiedad arquillian.launch no se aplica, así que del fichero arquillian.xml siempre se utiliza el bloque qualifier marcado con default=»true». Tendremos que crear las configuraciones de ejecución que necesitemos -Eclipse ya las va creando de forma automática cada vez que lanzamos un test- e incluir la propiedad -Darquillian.launch en los argumentos de la jvm.

Eclipse arquillian.launch
Problemas habituales

La conexión remota a WildFly es propensa a errores de configuración que, por experiencia propia, pueden hacernos perder bastante tiempo. En concreto, estos son los problemas más comunes.

MensajeCausa más probable
java.net.ConnectException: Connection refusedEl puerto web (parámetro port) no es correcto. Comprobar con un navegador que hay conexión.
WFLYPRT0053: Could not connect to remote+http://192.168.1.131:10090. The connection failedLa url de administración no es correcta, o bien se ha realizado la conexión pero no existe un usuario administrador. Comprobar con un navegador que se puede acceder y hacer login en la consola de administración con la url que indica el error, y que se corresponde con los parámetros de configuración managementAddress y managementPort.
WFLYPRT0053: Could not connect to remote+http://192.168.1.131:10090. The connection timed out.Hay conexión con el servidor pero el puerto de administración es incorrecto (managementPort), o se ha agotado el tiempo de espera (connectionTimeout). En este último caso, según mi experiencia, más habitual en Windows que en Linux, hay que probar aumentando el tiempo de espera (connectionTimeout).
javax.security.sasl.SaslException: Authentication failed: all available authentication mechanisms failedEste mensaje aparece en la traza del error WFLYPRT0053: Could not connect to remote+http://192.168.1.131:10090. The connection failed. Las credenciales son incorrectas.

WildFly embebido

Esta configuración es muy similar a la gestionada. Arquillian arranca WildFly pero dentro de la instancia de la máquina virtual de Java en la que se ejecutan las pruebas.

<profile>
	<id>arq-wildfly-embedded</id>
	<dependencies>
		<dependency>
			<groupId>org.wildfly.arquillian</groupId>
			<artifactId>wildfly-arquillian-container-embedded</artifactId>
			<version>${arquillian.wildlfy.version}</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_embedded</arquillian.launch>
					</systemPropertyVariables>
				</configuration>
			</plugin>
		</plugins>
	</build>
</profile>

En arquillian.xml indicamos la ubicación de WildFly y -esto es nuevo- la carpeta raíz de los módulos. Aquí no procede la utilización de javaVmArguments, pues la jvm usada para ejecutar WildFly ya está en ejecución.

<container qualifier="wildfly_embedded">
	<configuration>
		<property name="jbossHome">/opt/CURSO/SERVERS/wildfly-preview-22.0.0.Final.test</property>
		<property name="modulePath">/opt/CURSO/SERVERS/wildfly-preview-22.0.0.Final.test/modules</property>
	</configuration>
</container>

El diagrama con los perfiles queda así.

Pruebas como cliente

Hasta ahora, nuestra prueba se ha ejecutado dentro de WildFly con su código formando parte de la aplicación desplegada. Pero hay escenarios en los que las pruebas deben ejecutarse fuera del servidor porque necesitamos simular un cliente externo que realiza llamadas al mismo. Esta situación se da, por ejemplo, si queremos probar el consumo de EJB remotos o, el caso más habitual, peticiones HTTP a servicios REST, SOAP o incluso interfaces gráficas web (HTML).

La imagen subraya la ejecución de las pruebas fuera del artefacto.

Recuperemos el servlet creado en el capítulo tres adaptándolo al proyecto de ejemplo para que use la clase Message (los servlets admiten inyecciones).

package com.danielme.jakartaee.arquillian;

import jakarta.inject.Inject;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(HelloServlet.URL)
public class HelloServlet extends HttpServlet {

    public static final String URL = "helloServlet";

    @Inject
    private Message message;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                                throws ServletException, IOException {
        PrintWriter printWriter = response.getWriter();
        response.setContentType("text/plain;charset=UTF-8");
        printWriter.print(message.get());
    }

}

Una prueba más que razonable consiste en verificar que el servlet está disponible en la url esperada y devuelva el texto correcto.

@Test
@DisplayName("cliente HTTP que llama al servlet")
void testHelloServlet() throws IOException, InterruptedException {
     HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8180/arquillian-test/helloServlet"))
                .build();

     HttpResponse<String> response =
                HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());

     assertThat(response.body()).isEqualTo(HELLO_MESSAGE);
  }

No podemos olvidarnos de incluir HelloServlet en arquillian-test.war.

.addClass(Message.class)
.addClass(HelloServlet.class)

testHelloServlet espera que HelloServlet atienda en una URL -en breve analizaremos este asunto-, y Arquillian se encarga de ello. La prueba se ejecuta exitosamente con cualquiera de los perfiles, pero queremos que la llamada se haga desde fuera del servidor, simulando lo que haría un cliente del servlet. Hay dos formas de conseguirlo.

  • Anotar el método con @RunAsClient. Esto posibilita combinar en una misma clase pruebas locales (dentro del servidor) y remotas.
@Test 
@RunAsClient 
@DisplayName("cliente HTTP que llama al servlet") 
void testHelloServlet() throws IOException, InterruptedException { 
  • Indicar que el artefacto no es «testeable», esto es, que no contiene pruebas. En este caso, todas las pruebas de la clase se ejecutan de forma remota y no hay que anotarlas con @RunAsClient. La clase tampoco se incluye en el artefacto.
@Deployment(testable = false)
public static WebArchive createDeployment() {

Con independencia de la alternativa elegida, si echamos un vistazo a testHelloServlet vemos que hace falta su dirección completa. Es fácil ver que esto resulta problemático porque puede cambiar cualquiera de sus componentes.

  • La dirección IP del servidor.
  • El puerto HTTP. Recordemos que al arrancar WildFly sumamos 100 a los números de los puertos predeterminados.
  • El nombre de la aplicación, que coincide con el del artefacto.

Si queremos lanzar la prueba a conveniencia tanto en un WildFly local como en uno ubicado otra máquina, debemos encontrar el modo de generar la dirección adecuada de forma automática. Por fortuna, Arquillian provee una instancia de URL con los datos correspondientes al despliegue.

@ArquillianResource
private URL contextPath

Añadimos a MessageArquillianTest la versión final de la nueva prueba.

    @Test
    @RunAsClient
    @DisplayName("cliente HTTP que llama al servlet")
    void testHelloServlet() throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(contextPath + HelloServlet.URL))
                .build();

        HttpResponse<String> response =
                HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());

        assertThat(response.body()).isEqualTo(HELLO_MESSAGE);
    }

Por último, creemos también una nueva clase llamada HelloServletArquillianTest con la prueba testHelloServlet, pero en esta ocasión usando la opción testable=false. Ya no hace falta incluir AssertJ en el artefacto porque no contiene pruebas que la usen (de hecho, no contiene ninguna).

package com.danielme.jakartaee.arquillian;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(ArquillianExtension.class)
class HelloServletArquillianTest {

    private static final String HELLO_MESSAGE = "Jakarta EE rocks!!";

    @ArquillianResource
    private URL contextPath;

    @Deployment(testable = false)
    public static WebArchive createDeployment() {
        return ShrinkWrap.create(WebArchive.class, "arquillian-test.war")
                .addPackage(Message.class.getPackage());
    }

    @Test
    @DisplayName("cliente HTTP que llama al servlet")
    void testHelloServlet() throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(contextPath + HelloServlet.URL))
                .build();

        HttpResponse<String> response =
                HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());

        assertThat(response.body()).isEqualTo(HELLO_MESSAGE);
    }

}

Las nuevas pruebas se ejecutan de la forma habitual y deben ser exitosas.

Depuración

¿Qué sucede si ponemos puntos de ruptura o breakpoints en las pruebas y las ejecutamos en modo depuración? Depende tanto del tipo de prueba como del adaptador con el que Arquillian se comunica con WildFly.

  • Todas las pruebas que se ejecuten con un servidor embebido o en modo cliente se depuran en el IDE como cualquier código Java. La ejecución se detiene en los puntos de ruptura.
  • En los demás casos (pruebas que no sean de tipo cliente y además se ejecuten en modo gestionado o remoto), los puntos de ruptura se ignoran. El código de las pruebas está dentro de un servidor que a su vez se ejecuta en un proceso de la jvm distinto al que se emplea para lanzar las pruebas. Por este motivo, en realidad no estamos depurando nada.

Ante el segundo escenario, hay que arrancar WildFly habilitando la depuración remota con el protocolo jdwp en cierto puerto (vamos a usar 8888 y evitar posibles conflictos con el predeterminado 8787). En modo gestionado lo hacemos en el fichero arquillian.xml así

<property name="javaVmArguments">-Djboss.socket.binding.port-offset=100 -agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=y</property>

Con esta configuración (suspend=y), el arranque de WildFly quedará detenido en espera de una conexión de depuración remota para que nos dé tiempo a establecerla antes de que se ejecuten las pruebas a depurar. IntelliJ lo pone fácil al mostrar en la salida el botón «Attach Debugger».

IntelliJ Arquillian Attach Debugger

En cambio, si usamos Arquillian en modo remoto, activaremos la depuración en el WildFly al iniciarlo. No hace falta el parámetro suspend=y porque el servidor ya estará en ejecución cuando lancemos las pruebas.

(LINUX) ./standalone.sh -Djboss.socket.binding.port-offset=100 --debug 8888
(WINDOWS) standalone.bat -Djboss.socket.binding.port-offset=100 --debug 8888

Antes de ejecutar los tests, tenemos que conectar el depurador del IDE con WildFly, lo que requiere una configuración de depuración remota. En IntelliJ, nos vamos a la opción «Run->Edit Configurations…» del menú. Se abre una pantalla en la que tenemos que pulsar el botón «+» de la parte superior izquierda para desplegar un listado y seleccionar Remote JVM Debug.

IntelliJ add new configuration

En el formulario, indicamos el puerto y la dirección del servidor.

Seleccionamos la configuración y pulsamos el botón del «bichito». Mientras el servidor esté en ejecución, IntelliJ estará conectado en modo depuración a menos que pulsemos stop, y seremos capaces de depurar las pruebas que ejecutemos.

Los usuarios de Eclipse tienen que ir a la opción Run->Debug Configurations… para rellenar un formulario parecido.

eclipse debug configurations remote

Tras lanzar la configuración, Eclipse estará en modo depuración con el servidor hasta que este se detenga o reinicie.

Código de ejemplo

El código de ejemplo del tutorial completo se encuentra en GitHub (todos los proyectos están en un único repositorio). Incluye la configruación para el capítulo dedicado al testing con Docker. Para más información sobre cómo utilizar GitHub, consultar este artículo.

>>>> ÍNDICE <<<<

2 comentarios sobre “Curso Jakarta EE 9 (7). Pruebas automáticas (3): Testing en WildFly con Arquillian.

  1. No puedo creerlo, tú entrada es una joya para mí desde hace días estoy buscando la forma de implementar exactamente esto y ni siquiera sabía por dónde empezar, ni las tecnologías que podría implementar. Mil gracias por compartir tu conocimiento

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.