Curso Jakarta EE (20). JPA con Hibernate (3): Docker y pruebas automáticas.

logo Jakarta EE

Tal y como cabría esperar, las tediosas tareas de configuración de la infraestructura del proyecto que hicimos a mano en el capítulo anterior se pueden automatizar. Con Docker tendremos un entorno de ejecución completo, distribuible y a medida, útil tanto para desarrollar como para automatizar la ejecución de pruebas con Arquillian Cube. Todo son ventajas, pongámonos a ello.

>>>> ÍNDICE <<<<

Imagen Docker

Hemos aprendido a configurar conexiones a MySQL en WildFly, pero ¿cómo lo hacemos en nuestra flamante imagen? Lo más simple es utilizar la orden COPY para copiar en el servidor los ficheros del controlador JDBC, la declaración de la fuente de datos y el standalone.xml. Por comodidad, los pondremos a la altura del Dockerfile.

Y le añadimos al final las órdenes COPY.

RUN $JBOSS_HOME/bin/add-user.sh -u $ADMIN_USER -p $ADMIN_PASS -g admin --silent

EXPOSE 8080 9990 8787

COPY mysql-connector-java-8.0.25.jar $JBOSS_HOME/modules/system/layers/base/com/mysql/main/
COPY module.xml $JBOSS_HOME/modules/system/layers/base/com/mysql/main/
COPY standalone.xml $JBOSS_HOME/standalone/configuration/

CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0", "--debug", "*:8787"]

Construimos la imagen y ejecutamos un contenedor.

$ docker image build -t wildfly-mysql-copy .
$ docker run -d -p 8080:8080  -p 9990:9990 wildfly-mysql-copy

Sin embargo, considero más refinado descargar y configurar el driver JDBC y la fuente de datos durante el proceso de creación de la imagen. Así, solo necesitaremos el fichero Dockerfile. De paso, aprenderemos a hacer estas tareas con la herramienta de administración de WildFly por línea de comandos que ya empleamos por primera vez en el capítulo tres.

ENV DRIVER_VERSION 8.0.25
ENV JBOSS_CLI /opt/jboss/wildfly/bin/jboss-cli.sh

RUN   bash -c '$JBOSS_HOME/bin/standalone.sh &' 
      && bash -c 'until `$JBOSS_CLI -c ":read-attribute(name=server-state)" 2> /dev/null | grep -q running`; do echo `$JBOSS_CLI -c ":read-attribute(name=server-state)" 2> /dev/null`; sleep 1; done' 
      && curl --location --output /tmp/mysql-connector-java-${DRIVER_VERSION}.jar --url https://search.maven.org/remotecontent?filepath=mysql/mysql-connector-java/${DRIVER_VERSION}/mysql-connector-java-${DRIVER_VERSION}.jar 
      && $JBOSS_CLI --connect --command="module add --name=com.mysql --resources=/tmp/mysql-connector-java-${DRIVER_VERSION}.jar --dependencies=javax.api,javax.transaction.api" 
      && $JBOSS_CLI --connect --command="reload" 
      && $JBOSS_CLI --connect --command="/subsystem=datasources/jdbc-driver=mysql:add(driver-name=mysql,driver-module-name=com.mysql,driver-class-name=com.mysql.cj.jdbc.Driver)" 
      && $JBOSS_CLI --connect --command="data-source add --jndi-name=java:/jdbc/personalBudgetDS --jta="false" --name=personalBudgetDS --connection-url=jdbc:mysql://localhost:3307/personal_budget --driver-name=mysql --user-name=budget --password=budget  --initial-pool-size=1 --min-pool-size=1 --max-pool-size=5 --idle-timeout-minutes=5" 
      && $JBOSS_CLI --connect --command=":shutdown" 
      && rm -rf $JBOSS_HOME/standalone/configuration/standalone_xml_history/ $JBOSS_HOME/standalone/log/* 
      && rm -f /tmp/*.jar


CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0", "--debug", "*:8787"]

Lo anterior es la porción final del Dockerfile. En la línea cuatro se inicia WildFly y se espera a que el servidor responda. Seguidamente, se obtiene el controlador JDBC de MySQL desde mvnrepository para registrarlo en las líneas siete, ocho y nueve. Se hace lo propio con la fuente de datos en la línea diez. Por último, somos buenos chicos y limpiamos nuestras huellas: el servidor se detiene y se eliminan los ficheros temporales.

Sea cual sea la alternativa por la que nos decantemos a la hora de definir la imagen, con la dirección localhost WildFly será incapaz de conectarse a la base de datos aunque la tengamos operativa en nuestro equipo y toda la configuración de puertos y usuarios sea correcta. En un contenedor, localhost apunta sí mismo y no al anfitrión que es donde está MySQL, ya sea nativo o «dockerizado». En la bitácora del servidor veremos una traza de error así.

La solución es sencilla: en un contenedor se puede acceder al anfitrión con la dirección host.docker.internal

        <connection-url>jdbc:mysql://host.docker.internal:3307/personal_budget?autoReconnect=true&amp;useSSL=false&amp;allowPublicKeyRetrieval=true</connection-url>

En Linux, esto solo funcionará si se creó el contenedor con el parámetro --add-host host.docker.internal:host-gateway

$ docker run -d -p 8080:8080  --add-host host.docker.internal:host-gateway wildfly-mysql-copy

Y se añade esta línea al fichero hosts (sudo nano /etc/hosts).

172.17.0.1 host.docker.internal

Con este pequeño sortilegio, la fuente de datos de WildFly ya puede conectarse a un servidor de MySQL que tengamos en ejecución en nuestro equipo. La alternativa sofisticada consiste en la creación de una red para que los contenedores de WildFly y MySQL se «vean» mutuamente. Lo trataremos en la próxima sección.

Docker compose

WildFly y MySQL forman en conjunto el entorno de ejecución de nuestro proyecto y necesitamos disponer de ambos en todo momento, así que lo más práctico es integrarlos con Docker Compose. A continuación se muestra un fichero docker-compose.yml y que no es más una pequeña variación del que vimos cuando examinamos esta funcionalidad.

version: "3"
services:
    personal_budget-mysql:
        image: mysql:8.0.25
        ports:
            - 3307:3306   
        environment:
            - MYSQL_DATABASE=personal_budget
            - MYSQL_USER=budget
            - MYSQL_PASSWORD=budget
            - MYSQL_ROOT_PASSWORD=root
        volumes:
            - personal_budget-data:/var/lib/mysql
    personal_budget-wildfly:
        build:
            dockerfile: Dockerfile
            context: .
        depends_on:
             - personal_budget-mysql
        ports:
             - 8080:8080
             - 9990:9990
             - 8787:8787
volumes:
   personal_budget-data:

Nada que añadir a lo que ya conocemos. Definimos el servicio de MySQL, incluyendo la base de datos y sus credenciales, y el de WildFly. La imagen de este último se declara en un Dockerfile idéntico al de la sección anterior, excepto por la url de la base de datos.

--connection-url=jdbc:mysql://personal_budget-mysql:3306/personal_budget

Se ha usado el nombre del servicio (personal_budget-mysql) y el puerto 3306 gracias a que los dos contendores comparten una red. Desde fuera de ella, tenemos disponibles los puertos expuestos en el docker-compose.yml para que podamos trabajar con la consola de administración y conectarnos a la base de datos, en este escenario con el puerto 3307. Recuerdo, una vez más, que estamos creando un entorno de desarrollo y pruebas.

Los comandos más habituales para tenerlos a mano.

  • docker-compose up -d : levanta los contenedores. Se crean si no existen. Tiene en cuenta los cambios en el fichero docker-compose.yml
  • docker-compose stop : detiene los contenedores,
  • docker-compose down: elimina los contenedores y la red, pero no los volúmenes.
  • docker-compose up -d –build : la opción build fuerza la creación de la imagen desde el Dockerfile por si hemos realizado algún cambio.

Para desplegar las aplicaciones desde Eclipse o IntelliJ, nos sirve lo estudiado en el capítulo Desarrollo con contenedores, pero en el docker.compose.yml hay que definir un volumen que enlace la carpeta en la que IDE genera el artefacto .war del proyecto con el directorio /opt/jboss/wildfly/standalone/deployments del contenedor. Lo habitual es que esta carpeta coincida con el target de Maven.

    personal_budget-wildfly:
        build:
            dockerfile: Dockerfile
            context: .
        depends_on:
             - personal_budget-mysql
        ports:
             - 8080:8080
             - 9990:9990
             - 8787:8787
        volumes:
             - /opt/CURSO/DOCKER-JPA-pruebas/jpa-entity/target:/opt/jboss/wildfly/standalone/deployments

En el caso de IntelliJ, crearemos la configuración para Docker compose en vez de Dockerfile.

Arquillian Cube

Siguiendo la praxis habitual en el curso, probaremos los conceptos que vayamos aprendiendo con tests. Ya sabemos usar Arquillian y Arquillian Cube, por lo que vamos a recuperar en el proyecto de ejemplo (jpa-entity) sus configuraciones y perfiles Maven.

Ahora más que nunca, y por si el lector todavía no está convencido del todo, tiene sentido recurrir a Arquillian Cube para ejecutar las pruebas y delegar la gestión de los servidores. Con los perfiles que no usan Cube, en WildFly debe estar configurada la fuente de datos y el controlador JDBC. Y aunque dejemos que Arquillian gestione el servidor de aplicaciones, deberemos asegurar que MySQL esté accesible. Asimismo, la base de datos debe ser específica para las pruebas. Varios dolores de cabeza evitables.

En lugar de reutilizar el docker-compose anterior, es más flexible emplear el formato CUBE porque permite personalizar el funcionamiento del marco de trabajo. Esta es la configuración de los contenedores, preparada para que las pruebas se puedan ejecutar aunque estemos usando el docker-compose (atención a los puertos).

 <container qualifier="docker-wildfly_mysql">
     <configuration>
            <property name="managementPort">10999</property>
            <property name="username">admin</property>
            <property name="password">admin</property>
            <property name="port">8989</property>
            <property name="connectionTimeout">20000</property>
        </configuration>
     </container>

<extension qualifier="docker">
        <property name="autoStartContainers">personal_budget-wildfly-cube</property>
        <property name="definitionFormat">CUBE</property>
        <property name="dockerContainers">
            personal_budget-mysql-cube:
                image: mysql:8.0.25
                env: [MYSQL_DATABASE=personal_budget, MYSQL_USER=budget, MYSQL_PASSWORD=budget, MYSQL_ROOT_PASSWORD=root]
                await:
                    strategy: log
                    match: 'personal_budget'
                    timeout: 30
                portBindings: [3308->3306/tcp]
            personal_budget-wildfly-cube:
                buildImage:
                    dockerfileLocation: src/test/resources/docker
                    noCache: false
                portBindings: [8181->8080/tcp, 10999->9990/tcp, 8888/tcp]
                links:
                    - personal_budget-mysql-cube
        </property>
        <!-- Only for Windows -->
        <!-- Alternative: set the system var DOCKER_HOST. See the profile test-windows-docker-url in pom.xml-->
        <!--<property name="serverUri">tcp://localhost:2375</property>-->
    </extension>

Aparecen tres nuevas opciones con respecto a lo que estudiamos en su momento.

  • La declaración de las variables de entorno de la imagen se realiza de forma distinta a la de Docker Compose.
  • Await establece la estrategia que decide si el servicio se puede considerar disponible. Hasta ese momento, Arquillian no debe ejecutar las pruebas o bien arrancar otro servicio que lo necesite. Por omisión, se aplica polling que usa el comando ss dentro del contenedor para intentar obtener respuesta de uno de sus puertos. Por ese motivo, instalamos esa utilidad cuando creamos la imagen de WildFly. Con MySQL estamos usando la imagen oficial y no lo incluye. De todas formas, queremos verificar que la base de datos personal_budget se ha creado, y la estrategia más apropiada consiste en comprobar en la salida del contenedor -la bitácora de MySQL- la aparición de esa palabra. Damos un margen de 30 segundos hasta que ocurra.
  • Con links se vincula el servicio WildFly al de MySQL. El nombre de este último lo pondremos en la dirección del servidor en la definición de la fuente de datos en WildFly. El servidor se construye con el Dockerfile ubicado en src/test/resources/docker.

Comprobemos que la configuración es válida con esta prueba tan sencilla para DbInfo a ejecutar dentro WildFly.

package com.danielme.jakartaee.jpa;

import com.danielme.jakartaee.jpa.db.DbInfo;
import jakarta.inject.Inject;
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.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.junit.jupiter.api.Assertions.assertTrue;

@ExtendWith(ArquillianExtension.class)
class DbInfoArquillianTest {

    @Inject
    private DbInfo dbInfo;

    @Deployment
    public static WebArchive createDeployment() {
        return ShrinkWrap.create(WebArchive.class, "jpa-test.war")
                .addClass(DbInfo.class);
    }

    @Test
    void testVersion8() {
        assertTrue(dbInfo.getVersion().startsWith("8"));
    }

}

No necesitamos ni WildFly ni MySQL para ejecutarla, solo tener instalado Java 11y Docker, y usar el wrapper de Maven que incluyo en todos los proyectos del curso.

$ ./mvnw test -P arq-docker

Vamos a probar la disponibilidad de JPA, que es lo que nos interesa. Me adelanto un poco, y voy a usar el gestor de entidades en la siguiente clase.

package com.danielme.jakartaee.jpa;

import com.danielme.jakartaee.jpa.entities.Expense;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
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.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.asset.FileAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.io.File;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@ExtendWith(ArquillianExtension.class)
class JpaArquillianTest {

    @PersistenceContext
    private EntityManager em;

    @Deployment
    public static WebArchive createDeployment() {
        return ShrinkWrap.create(WebArchive.class, "jpa-test.war")
                .addClass(Expense.class)
                .addAsResource(new FileAsset(new File("src/main/resources/META-INF/persistence.xml")), "/META-INF/persistence.xml")
                .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
    }

    @Test
    void testFindExpense() {
        Expense expense = em.find(Expense.class, 1L);
        
        assertNotNull(expense);
    }
}

Aquí se está desplegando un artefacto que solo contiene la entidad Expense y el fichero persistence.xml. El gestor de entidades se inyecta con la anotación especial @PersistenceContext. En testFindExpense le estamos pidiendo que devuelva un objeto\registro de Expense con el identificador 1.

La prueba solo será exitosa si en la tabla expenses existe un gasto de clave primaria «1». Se podría crear la imagen del contenedor de MySQL con un script que cree tanto la estructura de la base de datos como los registros que los tests esperan. No obstante, esta técnica casi nunca nos servirá: por ejemplo, en otra prueba es posible que necesitemos que la tabla de gastos esté vacía.

DBUnit con Database Rider

La solución a nuestros problemas de persistencia pasa por ejecutar antes de cada prueba un script que asegure que la base de datos se encuentre en el estado esperado. Arquillian cuenta con la extensión persistence para asistirnos en el desarrollo de tests de integración que hagan uso de una capa de persistencia relacional. Lastimosamente, es una librería «ancestral» que no se actualiza desde 2014 y presenta incompatibilidades de todo tipo con las dependencias actuales de Arquillian para Jupiter \ JUnit 5.

No levantemos los brazos todavía: podemos recurrir a la librería DBUnit en la que Arquillian Persistence se basa y usarla a través del proyecto Database Rider y su integración con Jupiter. Eso sí, nos tocará hacer un poco de bricolaje para solventar algunos problemas.

¡Manos a la obra! Empecemos por la dependencia.

   <dependency>
            <groupId>com.github.database-rider</groupId>
            <artifactId>rider-junit5</artifactId>
            <version>${databaserider.version}</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-commons</artifactId>
                </exclusion>
                <exclusion>
                	<groupId>org.junit.jupiter</groupId>
                	<artifactId>junit-jupiter-engine</artifactId>
                </exclusion>
                <exclusion>
                	<groupId>org.apache.poi</groupId>
                	<artifactId>poi-ooxml</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

Hay que realizar varias exclusiones para evitar conflictos con las versiones de otras librerías que tenemos en el pom. La última de ellas causa problemas cuando la prueba se ejecuta dentro de WildFly.

En el fichero /src/test/resources/dbunit.yml se configura DbUnit. Estos parámetros se pueden sobrescribir para una clase de pruebas con la anotación @DbUnit.

cacheConnection: false
properties:
  caseSensitiveTableNames: true

He modificado la forma en la que DBUnit genera el nombre de las tablas (en mayúsculas). En general, el lenguaje SQL ignora la capitalización, pero en las tablas de MySQL en Linux sí se tiene en cuenta. También he desactivado la caché interna de conexiones porque he observado que algunas no se cerraban. Recomiendo al lector revisar todas las opciones configurables, en más de una ocasión nos pueden sacar de un apuro.

El siguiente paso es ampliar la clase de pruebas con tres elementos. Primero veremos el caso sencillo: pruebas que se ejecutan en modo cliente, esto es, fuera del servidor. De ahí que la propiedad testeable sea falsa.

package com.danielme.jakartaee.jpa;

import com.danielme.jakartaee.jpa.entities.Expense;
import com.github.database.rider.core.api.connection.ConnectionHolder;
import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.junit5.DBUnitExtension;
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.asset.FileAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.io.File;
import java.sql.*;

import static org.junit.jupiter.api.Assertions.assertTrue;

@ExtendWith(ArquillianExtension.class)
@ExtendWith(DBUnitExtension.class)
class ClientDbUnitArquillianTest {

    ConnectionHolder buildConnectionHolder() {
        return () -> DriverManager.getConnection("jdbc:mysql://localhost:3308/personal_budget" +
                        "?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"
                , "budget"
                , "budget");
    }

    @Deployment(testable = false)
    public static WebArchive createDeployment() {
        return ShrinkWrap.create(WebArchive.class, "jpa-test.war")
                .addClass(Expense.class)
                .addAsResource(new FileAsset(new File("src/main/resources/META-INF/persistence.xml")),
                        "/META-INF/persistence.xml");
    }

    @Test
    @DataSet(value = "/datasets/expenses.yml")
    void testFindExpense() throws SQLException {
        try (Connection connection = buildConnectionHolder().getConnection();
             PreparedStatement ps = connection.prepareStatement("SELECT id from expenses where id = ?");) {
            ps.setLong(1, 1L);
            ResultSet resultSet = ps.executeQuery();

            assertTrue(resultSet.next());
        }
    }

}
  • Tenemos que «enriquecer» las pruebas con la extensión DBUnitExtension para que DataBase Rider haga su magia.
  • Se requiere atributo de tipo ConnectionHolder o un método que lo retorne. Su finalidad es proporcionar la conexión a la base de datos. He dejado la cadena de conexión en el mismo código para que el ejemplo se vea más claro, pero esos parámetros habría que definirlos de tal forma que sean fáciles de cambiar y reutilizar.
  • La anotación @DataSet, aplicable tanto a un método como a la clase, es la clave de todo. Aquí se indican los ficheros con los juegos de datos en formato JSON, YAML o XML. He optado por YAML, creo que es lo más legible. Su contenido es el siguiente.
expenses:
  - id: 1
    amount: 120.43
    concept: "Lunch menu"
    date : 2021-06-05

El método testFindExpense se conecta a la base de datos y verifica la existencia del gasto del dataset. Si todo va bien, la prueba será exitosa.

Volvamos a JpaArquillianTest . Contiene una prueba que se ejecuta en el servidor. Podemos aprovecharnos de esta circunstancia e inyectar la fuente de datos que proporciona WildFly, la misma que usará la aplicación (y JPA) cuando la despleguemos. Lo que sí que es imprescindible es la inclusión de las dependencias de DataBase Rider en el artefacto.

package com.danielme.jakartaee.jpa;

import com.danielme.jakartaee.jpa.entities.Expense;
import com.github.database.rider.core.api.connection.ConnectionHolder;
import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.junit5.DBUnitExtension;
import jakarta.annotation.Resource;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
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.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.asset.FileAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import javax.sql.DataSource;
import java.io.File;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@ExtendWith(ArquillianExtension.class)
@ExtendWith(DBUnitExtension.class)
class JpaArquillianTest {

    @Resource(lookup = "java:/jdbc/personalBudgetDS")
    private DataSource dataSource;

    @PersistenceContext
    private EntityManager em;

    private Connection connection;

    ConnectionHolder buildConnectionHolder() {
        return () -> {
            if (connection == null || connection.isClosed()) {
                connection = dataSource.getConnection();
            }
            return connection;
        };
    }

    @Deployment
    public static WebArchive createDeployment() {
        File[] dbRider = Maven.resolver()
                .loadPomFromFile("pom.xml")
                .resolve("com.github.database-rider:rider-junit5")
                .withTransitivity()
                .asFile();

        return ShrinkWrap.create(WebArchive.class, "jpa-test.war")
                .addClass(Expense.class)
                .addClass(ArquillianDBUnitExtension.class)
                .addAsWebInfResource(new File("src/test/resources/datasets/expenses.yml"), "classes/datasets/expenses.yml")
                .addAsWebInfResource(new File("src/test/resources/dbunit.yml"), "classes/dbunit.yml")
                .addAsResource(new FileAsset(new File("src/main/resources/META-INF/persistence.xml")),
                        "/META-INF/persistence.xml")
                .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
                .addAsLibraries(dbRider);
    }

    @Test
    @DataSet(value = "/datasets/expenses.yml")
    void testFindExpense() {
        Expense expense = em.find(Expense.class, 1L);

        assertNotNull(expense);
    }

}

Parece todo correcto, pero no funciona. Obtendremos un hermoso NullPointer.

El objeto dataSource es nulo y no se ha inyectado. Por muchas vueltas que le demos, la configuración es correcta y hemos topado con un problema al ejecutar pruebas de JUnit 5 en un contenedor con Arquillian. Los puntos de extensión, como ese beforeTestExecution que aparece en la traza, se ejecutan dos veces: la primera vez como parte de la clase que Maven o el IDE realmente ejecuta, y una segunda vez cuando la prueba se efectúa en el servidor. Es decir, tenemos dos instancias distintas de la clase, y la que no está en el servidor no recibe las inyecciones por motivos obvios. Ese es el origen del NullPointer.

Podemos sortear este obstáculo usando la misma implementación de buildConnectionHolder que en ClientDbUnitArquillianTest, pero seguimos con el problema de fondo y el juego de datos se intentará cargar dos veces. La solución definitiva pasa por en conseguir que DBUnitExtension solo actúe cuando la prueba se ejecuta en el servidor. He encontrado una manera poco elegante pero útil: crear mi propia versión de la extensión.

package com.danielme.jakartaee.jpa.extensions;

import com.github.database.rider.junit5.DBUnitExtension;
import org.junit.jupiter.api.extension.ExtensionContext;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class ArquillianDBUnitExtension extends DBUnitExtension {

    private boolean isInside() {
        try {
            new InitialContext().lookup("java:comp/env");
            return true;
        } catch (NamingException ex) {
            return false;
        }
    }

    @Override
    public void beforeTestExecution(ExtensionContext extensionContext) throws Exception {
        if (isInside()) {
            super.beforeTestExecution(extensionContext);
        }
    }

    @Override
    public void afterTestExecution(ExtensionContext extensionContext) throws Exception {
        if (isInside()) {
            super.afterTestExecution(extensionContext);
        }
    }

    @Override
    public void beforeEach(ExtensionContext extensionContext) throws Exception {
        if (isInside()) {
            super.beforeEach(extensionContext);
        }
    }

    @Override
    public void afterEach(ExtensionContext extensionContext) throws Exception {
        if (isInside()) {
            super.afterEach(extensionContext);
        }
    }

    @Override
    public void beforeAll(ExtensionContext extensionContext) throws Exception {
        if (isInside()) {
            super.beforeAll(extensionContext);
        }
    }

    @Override
    public void afterAll(ExtensionContext extensionContext) throws Exception {
        if (isInside()) {
            super.afterAll(extensionContext);
        }
    }

}

Heredo de DBUnitExtension para invocar a los puntos de extensión que implementa solo si la clase se ejecuta en un servidor de aplicaciones. Esto último lo averiguamos comprobando si tenemos acceso a algún elemento característico del mismo, como una clase, variable de entorno, etc. La opción que me parece más fiable es comprobar la existencia del contexto raíz utilizado para ciertos componentes de los entornos JEE accesibles por JNDI, sugerencia que encontré aquí.

Todo listo, solo nos resta aplicar la nueva extensión a nuestra clase.

@ExtendWith(ArquillianExtension.class)
@ExtendWith(ArquillianDBUnitExtension.class)
class JpaArquillianTest {

No podemos olvidarnos de incluirla en el artefacto.

.addClass(ArquillianDBUnitExtension.class)

Código de ejemplo

El código de ejemplo del capítulo se encuentra en GitHub (todos los proyectos son independientes pero 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 )

Google photo

Estás comentando usando tu cuenta de Google. 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.