Mostrando entradas con la etiqueta calidad. Mostrar todas las entradas
Mostrando entradas con la etiqueta calidad. Mostrar todas las entradas

domingo, 22 de agosto de 2021

Tests unitarios y de integración para JPA DAO

Hoy vamos a ver principalmente como hacer pruebas unitarias y de integración para la capa de acceso a datos, DAO, en una aplicación de JPA. Pero también veremos otros aspectos interesantes sobre pruebas que impliquen BBDD, como  puede ser el uso de librerías para la creación de un contexto JNDI hasta incluso BBDD para pruebas. 

Para empezar debemos aclarar un poco que son los test unitarios y que son los test de integración, con la ayuda de estas versiones resumidas de su definición: 

  • Test Unitario: Es aquel destinado a probar un único componente del código. El cual se puede ejecutar sin necesidad de otras dependencias.
  • Test de Integración: Es aquel destinado a probar una funcionalidad compleja del código. Implicará  la iteración entre distintos componentes e inclusos con sistemas externos al código. 

Otro de los puntos a tener en cuenta, es que los test serán sobre tecnología de Jakarta EE y no con Spring. Por lo que serán un poco más elaborados. Además cuando queramos realizar los tests de integración diferenciaremos entre si queremos utilizar una conexión JDBC o acceder a la BBDD a través de JNDI. Esta última solución será aplicable a otros tipos de tests que necesiten del acceso a través de JNDI, de hay que hagamos un ejemplo diferente para probar lo mismo.

Empezaremos creando la clase Entity que será un reflejo de una tabla que tenemos en la BBDD. 

@Data
@Entity
@Table(name = "BOOK")
@NamedQuery(name = "Book.findAll", query = "SELECT b FROM Book b")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private String author;
}

El siguiente paso será crear nuestra clase de acceso a datos. Esta parte la realizaremos en dos pasos. Primero crearemos una clase abstracta y genérica que contenga los métodos comunes. Esta clase implementará una interfaz que es la que declarará los métodos que tiene. 

public abstract class GenericDaoImpl<K, E> implements GenericDao<K, E> {

    private Class<E> persistentClass;

    @SuppressWarnings("unchecked")
    public GenericDaoImpl() {
	Type genericSuperClass = getClass().getGenericSuperclass();

	ParameterizedType parametrizedType = null;
	while (parametrizedType == null) {
	    if (genericSuperClass instanceof ParameterizedType) {
		parametrizedType = (ParameterizedType) genericSuperClass;
	    } else {
		genericSuperClass = ((Class<?>) genericSuperClass).getGenericSuperclass();
	    }
	}
	this.persistentClass = (Class<E>) parametrizedType.getActualTypeArguments()[1];
    }
    @Override
    public List<E> findAll() {
	return getEntityManager().createNamedQuery(
			String.format("%s.findAll", persistentClass.getSimpleName()), 
		persistentClass).getResultList();
    }
    @Override
    public E getByField(final String field, final String value) {
	return getEntityManager()
.createQuery(
			String.format("SELECT t FROM %s t where t.%s = :value", 
			persistentClass.getSimpleName(), field), persistentClass)
		.setParameter("value", value).getResultStream().findFirst().orElse(null);
    }
    @Override
    public E getById(final K key) {
	return getEntityManager().find(persistentClass, key);
    }
}

A continuación crearemos la clase de acceso a datos asociada a nuestra entidad. La cual extenderá la clase anterior y además se le incluirá la dependencia de la clase EntityManager, la cual nos permitirá acceder propiamente a los datos. 

@Stateless
public class BookDaoImpl extends GenericDaoImpl<Integer, Book> {
    @PersistenceContext(unitName = "persistence-unit")
    protected EntityManager em;

    @Override
    public EntityManager getEntityManager() {
	return em;
    }
}

Esta clase será identificada como un componente sin estado a ser gestionado por el servidor a través de la anotación @Stateless, la cual también nos permitirá inyectarla en otras clases. Aparte le será inyectada la clase EntityManager, cuyo comportamiento es definido en el fichero META-INF/persistence.xml y que nos dará el acceso a la BBDD y la gestión de transacciones. 

Empecemos ya con los ejemplos. Primero irá el test unitario, donde no necesitaremos ninguna BBDD o proveedor de persistencia. Verificaremos el funcionamiento de los métodos a través de mocks. Por tanto, estas serán las únicas librerías que necesitemos: 

  • La especificación de Java EE: javax.javaee-api.
  • Librerías propias de Testing: Junit para los tests, Hamcrest para las verificaciones y Mockito para la simulación de objetos. 
@ExtendWith(MockitoExtension.class)
public class GenericDaoImplUnitTest {
    BookDaoImpl dao;
    @Mock
    EntityManager eManager;
    @Mock
    TypedQuery<Book> mockQuery;

    private List<Book> _getBooks() {
	List<Book> lst = new ArrayList<Book>();
	lst.add(new Book());
	return lst;
    }
    @Test
    public void findAll() {
	when(dao.em.createNamedQuery(ArgumentMatchers.anyString(), ArgumentMatchers.eq(Book.class)))
		.thenReturn(mockQuery);
	when(mockQuery.getResultList()).thenReturn(_getBooks());

	List<Book> lst = dao.findAll();
	assertEquals(1, lst.size());
	verify(dao.em).createNamedQuery("Book.findAll", Book.class);
    }
    @BeforeEach
    public void setUp() {
	dao = new BookDaoImpl();
	dao.em = eManager;
    }
}

Estos métodos verificarán el correcto funcionamiento. Pero no podremos testear el funcionamiento del propio EntityManager, su configuración, o en caso de consultas más complejas si están correctamente desarrolladas. Por todo ello realizaremos a continuación un ejemplo de test de integración, en cual nos conectaremos a una BBDD, probando de esta forma la configuración y funcionamiento real de las consultas. 

Para poder llevar a cabo este tipo de pruebas, necesitaremos las siguientes librerías:

  • Un proveedor de persistencia, es decir la implementación de la API de JPA. En este caso utilizaremos org.eclipse.persistence.eclipselink.
  • Una base de datos. Para nuestro ejemplo usaremos com.h2database.h2.
El punto a favor de utilizar una BBDD, que podemos almacenar en memoria, como H2, es que podremos crear un test hibrido entre prueba unitaria y prueba de integración. Esto es debido a que evita que tengamos conexión con un sistema real, lo cual a veces puede ser problemático por que no exista entorno de pruebas o conexión directa. Además, los tests mantienen su aislamiento y al utilizar un sistema externo sin estado, verificando así el mismo comportamiento en cada ejecución.

En este caso, al arrancar la BBDD queremos que sea poblada con determinada información que nos permita realizar los tests. Para ello le indicaremos en la URL JDBC que ejecute el fichero schema-generator.sql. Por lo demás la configuración es bastante estándar.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="persistence-unit-test" transaction-type="RESOURCE_LOCAL">
        <class>com.home.example.dbjunittest.entity.Book</class>
        <properties>
            <property name="javax.persistence.jdbc.url" 
           	value="jdbc:h2:mem:test:sample;DB_CLOSE_ON_EXIT=FALSE;INIT=RUNSCRIPT FROM 'classpath:schema-generator.sql';" />
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="javax.persistence.schema-generation.database.action" value="none" />
            
            <property name="eclipselink.logging.level.sql" value="FINE"/>
            <property name="eclipselink.logging.parameters" value="true"/>
</properties> </persistence-unit> </persistence>

Y a continuación elaboraremos nuestro test de integración que hará uso de esta configuración.

public class BookDaoImplITTest {
    private static BookDaoImpl dao;

    @BeforeAll
    public static void before() {
	dao = new BookDaoImpl();
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence-unit-test");
	dao.em = emf.createEntityManager();
    }
    @Test
    public void findAll() {
	List<Book> lst = dao.findAll();
	assertThat(lst.size(), equalTo(1));
    }
}

Ahora veremos una modificación de este último ejemplo, utilizando en vez de una conexión JDBC, una conexión a través de JNDI. Pero para ello tendremos que añadir una nueva librería: com.github.h-thurow.simple-jndi. Esta librería nos permitirá poblar al objeto InitialContext con un Datasource que nosotros indiquemos, para que así pueda ser utilizado por el Entity Manager. 

El primer paso en este nuevo ejemplo es modificar el fichero persistence.xml, para indicar que dejamos de utilizar una conexión JDBC y queremos usar una JNDI. 

<persistence-unit name="persistence-unit-test" transaction-type="RESOURCE_LOCAL">
	<non-jta-data-source>java:comp/env/jdbc/myds</non-jta-data-source>
    <class>com.home.example.jpajndiinttest.entity.Book</class>
    <properties>
        <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
        <property name="javax.persistence.schema-generation.database.action" value="none" />
        
        <property name="eclipselink.logging.level.sql" value="FINE"/>
		<property name="eclipselink.logging.parameters" value="true"/>
    </properties>
</persistence-unit>

Para que la carga del Datasource en el contexto funcione correctamente debemos configurar dos ficheros. Uno será el encargado de configurar la librería que hemos indicado a través de un fichero denominado jndi.properties. En el aparte de configuración propia de la librería, indicaremos los caracteres de separación para la nomenclatura, el espacio de almacenaje de dicha información, y donde se ubicarán los ficheros para los distintos objetos a crear.  

java.naming.factory.initial=org.osjava.sj.SimpleContextFactory
org.osjava.sj.jndi.shared=true
org.osjava.sj.delimiter=.
jndi.syntax.separator=/
org.osjava.sj.space=java:comp/env
org.osjava.sj.root=src/test/resources/jndi

El segundo fichero, será el que configure el Datasource y en base a lo indicado anteriormente debe cumplir las siguientes reglas y contenido:

  • Debe estar almacenado en la carpeta src/test/resources/jndi
  • Debe llamarse jdbc.properties
  • Cada una de las propiedades del Datasource deben tener el sufijo myds. 

myds.type=javax.sql.DataSource
myds.driver=org.h2.Driver
myds.url=jdbc:h2:mem:test:sample;DB_CLOSE_ON_EXIT=FALSE;INIT=RUNSCRIPT FROM 'classpath:schema-generator.sql';
myds.user=sa
myds.password=password

Ya solo nos quedará realizar el test de la siguiente forma:

public class BookDaoImplIT {

    static BookDaoImpl dao;
    static InitialContext initContext;

    @BeforeAll
    public static void setup() throws Exception {
	initContext = new InitialContext();
	dao = new BookDaoImpl();
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence-unit-test");
	dao.em = emf.createEntityManager();
    }
    @Test
    public void findAll() throws InterruptedException {
	List<Book> lst = dao.findAll();
	assertThat(lst.size(), equalTo(1));
    }
}

Debemos tener en cuenta que ambos ejemplos no gestionan las transacciones al ser de tipo local. Por lo que si queremos hacer ejemplos más complejos, deberemos gestionar las transacciones manualmente. Esto es debido a que si utilizamos conexiones JTA, la clase EntityManagerFactory no funcionará. 

Indicar también que si queremos ejecutar los este tipo en una fase posterior o de forma independiente, también lo podemos hacer. Para ello tendremos que utilizar maven-failsafe-plugin. Y deberemos seguir las siguientes instrucciones:

  • El test debe seguir el siguiente patrón: "**/IT*.java, **/*IT.java o **/*ITCase.java
  • Debemos asociar su ejecución a una de las fases del ciclo de vida de Maven.

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-failsafe-plugin</artifactId>
	<version>2.22.2</version>
	<executions>
		<execution>
			<goals>
				<goal>integration-test</goal>
			</goals>
		</execution>
	</executions>
</plugin>

Como veis no solo hemos aprendido a hacer tests a la capa de acceso a datos sino que por el camino hemos aprendido un par detalles interesantes. Además, en el enlace al código fuente, podréis ver un test de cada uno los métodos para los distintos ejemplos, aquí. 

sábado, 14 de diciembre de 2019

PMD: Como hacer un custom rule

Hoy vamos a ver un poco de PMD, una magnífica herramienta que nos permite medir la calidad de nuestro código.  Es en general una herramienta imprescindible en nuestro día a día y muy completa si hablamos de código Java. Pero no tanto para otros lenguajes, aunque de soporte para ellos. Por tanto hoy hablaremos de cómo crear una regla, pero para XML. 

Para empezar debemos tener un concepto claro: Abstract Syntax Tree, AST. Este es el árbol de elementos de los que se compone nuestro código. Es decir, a través de él, podremos identificar cada uno de los elementos de nuestro código y por tanto asociar una regla que nos indique si esta bien creado el código o no. Para ayudarnos en esta tarea, tenemos la herramienta PMD Rule Designer, con la cual tras pasarle un trozo de código, nos devolverá su AST asociado.


Una vez tenemos claro cual es la regla que queremos implementar y sobre qué elemento del código queremos implementarla, pasaremos a escribirla. La implementación de la regla la podemos realizarla en código Java o a través de XPath. Pero su definición,  deberemos crearla siempre en XML.

Esta definición de la regla, se debe incluir en un fichero que contenga un conjunto de reglas o ruleset. Este ruleset puede ya existir (por ejemplo el básico para XML) o crearlo nosotros desde cero.

<?xml version="1.0"?>
<ruleset name="XML Own Convention"
 xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
 <description>Rules to detect if our XML is how we want.</description>
</ruleset>

Ahora nos quedaría crear la regla o rule. En ella debemos indicar diferentes atributos informativos de la regla como pueden ser el nombre, mensaje asociado cuando se muestre al usuario que la regla está siendo violada o un ejemplo de cómo podemos evitar la violación de la misma.

Si quisiéramos generarla con Java, escribiríamos una clase que heredase de net.sourceforge.pmd.lang.java.rule.AbstractJavaRule. Y que recibiese el elemento sobre el cual queremos realizar el análisis. Esta clase debería ser indicada en la definición de la regla a través del atributo class.

<rule name="WhileLoopsMustUseBracesRule"
  message="Avoid using 'while' statements without curly braces"
  class="WhileLoopsMustUseBracesRule">
 // description, priority or example properties
</rule>

En el caso de querer hacerla a traves de XPath. En el atributo class debemos indicar net.sourceforge.pmd.lang.rule.XPathRule y posteriormente crear una propiedad denominada xpath que incluya la regla que queremos validar. Para nuestro ejemplo, queremos verificar por ejemplo que una secuencia de WSO2, termine siempre con el sufijo '_seq'. Para ello implementaremos una regla XPath que realice exactamente lo contrario, que busque si dado un elemento sequence, su atributo name no termine en '_seq'.

<rule name="sequenceBadNamed" language="xml" 
 message="Incorrect sequence name."
 class="net.sourceforge.pmd.lang.rule.XPathRule">
 <description>A sequence was wrongly named, it must end with _seq</description>
 <priority>3</priority>
 <properties>
  <property name="xpath">
   <value>
    <![CDATA[/sequence[not(ends-with(@name, '_seq'))]]]>
   </value>
  </property>
 </properties>
 <example>
  <![CDATA[A sequence must be called: $Service_$SeqName_seq]]>
 </example>
</rule>

Como se puede ver el uso de XPath te puede permitir realizar reglas muy potentes y de forma sencilla, sin la necesidad de crear más clases.

Por último nos queda configurar el plugin de maven para el uso de validaciones con XML y nuestro conjunto de reglas. Para ello debemos:
  • Añadir como dependencia la librería específica de PMD para dicho lenguaje, en nuestro caso pmd-xml.
  • Configurar correctamente el apartado configuración.
    • Indicar que son validaciones para XML.
    • Indicar nuestro conjunto de reglas.
    • Indicar que los ficheros XML sean analizados. 

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-pmd-plugin</artifactId>
 <version>3.12.0</version>
 <dependencies>
  <dependency>
   <groupId>net.sourceforge.pmd</groupId>
   <artifactId>pmd-xml</artifactId>
   <version>6.3.0</version>
  </dependency>
 </dependencies>
 <configuration>
  <language>xml</language>
  <rulesets>
   <ruleset>${project.basedir}/src/test/resources/ownConvention.xml</ruleset>
  </rulesets>
  <includes>
   <include>**/*.xml</include>
  </includes>
 </configuration>
</plugin>