3.5 implementación de pruebas de integración con spring ... · la idea de hacer las pruebas con...

39
3.5 Implementación de Pruebas de Integración con Spring y JUnit

Upload: lamthuy

Post on 15-Oct-2018

217 views

Category:

Documents


0 download

TRANSCRIPT

3.5 Implementación de Pruebas de Integración con Spring y JUnit

Índice

Código de pruebas en Maven

JUnit

JUnit 4

Inicialización y borrado de datos

Spring TestContext Framework

Configuración

Inyección de dependencias

Transacciones

Integración con JUnit 4

Spring + Junit

Inicialización y borrado de datos

Transacciones

Código de Pruebas en Maven

Con Maven el código de pruebas se ubica en el directorio src/test (que tiene una estructura de directorios similar a la del directorio src/main)

En el módulo pojo-minibank el código de las clases de prueba está contenido en el paquete es.udc.pojo.minibank.test.model (y subpaquetes)

Las pruebas se pueden ejecutar automáticamente desde Maven (a través del plugin surefire) haciendo que se ejecute la fase test (e.g. mvn test) Maven genera un informe detallado en target/surefire-reports

Por defecto, se consideran clases de pruebas aquellas clases de src/test/java cuyo nombre empieza o termina por “Test”

JUnit

JUnit es un framework para escribir pruebas de unidad o de integración automatizadas en Java

http://www.junit.org/

Open Source

Programado por Erich Gamma y Kent Beck

Utiliza aserciones para comprobar resultados esperados

Tras la ejecución de las pruebas genera un informe indicando el número de pruebas ejecutadas y cuales no se han ejecutado satisfactoriamente

Existen dos tipos de fallos diferentes para una prueba

Failure: Indica que ha fallado una aserción, es decir, el código que se está probando no está devolviendo los resultados esperados, por lo que falla la comparación de los resultados

Error: Indica que ha ocurrido una excepción no esperada y que por tanto no se está capturando (e.g. NullPointerException o ArrayIndexOutOfBoundsException)

es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (1)

...

import static org.junit.Assert.assertEquals;

import static org.junit.Assert.assertTrue;

...

public class AccountServiceTest {

...

private AccountService accountService;

...

@BeforeClass

public static void populateDb() {

DbUtil.populateDb();

}

@AfterClass

public static void cleanDb() throws Exception {

DbUtil.cleanDb();

}

es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (1)

@Test

public void testCreateAccount() throws InstanceNotFoundException {

Account account = accountService.createAccount(new Account(1, 10));

Account account2 = accountService.findAccount(account.getAccountId());

assertEquals(account, account2);

}

@Test

public void testFindAccount() throws InstanceNotFoundException {

Account account = accountService.findAccount(DbUtil.getTestAccountId());

Account account2 = accountDao.find(DbUtil.getTestAccountId());

assertEquals(account2, account);

}

@Test(expected = InstanceNotFoundException.class)

public void testFindNonExistentAccount() throws InstanceNotFoundException {

accountService.findAccount(NON_EXISTENT_ACCOUNT_ID);

}

es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (2)

@Test

public void testWithdrawFromAccount()

throws InstanceNotFoundException, InsufficientBalanceException {

testAddWithdraw(false);

}

private void testAddWithdraw(boolean add)

throws InstanceNotFoundException, InsufficientBalanceException {

/* Perform operation. */

double amount = 5;

double newBalance;

Calendar startDate;

Calendar endDate;

Account testAccount = accountService.findAccount(DbUtil

.getTestAccountId());

if (add) {

newBalance = testAccount.getBalance() + amount;

} else {

newBalance = testAccount.getBalance() - amount;

}

startDate = Calendar.getInstance();

es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (3)

if (add) {

accountService.addToAccount(

testAccount.getAccountId(), amount);

} else {

accountService.withdrawFromAccount(

testAccount.getAccountId(), amount);

}

endDate = Calendar.getInstance();

/* Check new balance. */

testAccount = accountService.findAccount(testAccount.getAccountId());

assertTrue(newBalance == testAccount.getBalance());

es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (4)

/* Check account operation. */

List<AccountOperation> accountOperations =

accountService.findAccountOperationsByDate(

testAccount.getAccountId(),startDate, endDate, 0, 2);

assertTrue(accountOperations.size() == 1);

AccountOperation accountOperation = accountOperations.get(0);

if (add) {

assertEquals(AccountOperation.Type.ADD,

accountOperation.getType());

} else {

assertEquals(AccountOperation.Type.WITHDRAW,

accountOperation.getType());

}

assertTrue(amount == accountOperation.getAmount());

}

es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (5)

@Test(expected = InstanceNotFoundException.class)

public void testWithdrawFromNonExistentAccountAccount()

throws InstanceNotFoundException, InsufficientBalanceException {

accountService.withdrawFromAccount(NON_EXISTENT_ACCOUNT_ID, 10);

}

es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (6)

@Test

public void testWithdrawWithInsufficientBalance()

throws InstanceNotFoundException, InsufficientBalanceException {

boolean exceptionCatched = false;

Calendar startDate;

Calendar endDate;

/* Try to withdraw. */

startDate = Calendar.getInstance();

Account testAccount = accountService.findAccount(

DbUtil.getTestAccountId());

double initialBalance = testAccount.getBalance();

try {

accountService.withdrawFromAccount(

testAccount.getAccountId(), testAccount.getBalance() + 1);

} catch (InsufficientBalanceException e) {

exceptionCatched = true;

}

endDate = Calendar.getInstance();

assertTrue(exceptionCatched);

es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (y 7)

/* Check balance has not been modified. */

testAccount = accountService.findAccount(testAccount.getAccountId());

assertTrue(testAccount.getBalance() == initialBalance);

/* Check account operation has not been registered. */

List<AccountOperation> accountOperations =

accountService.findAccountOperationsByDate(

testAccount.getAccountId(), startDate, endDate, 0, 1);

assertTrue(accountOperations.size() == 0);

}

...

}

JUnit 4 (1)

Requiere Java SE 5.0 o superior

Utiliza anotaciones @Test para marcar un método

como un caso de prueba El parámetro timeout permite especificar que el test falle si

su ejecución no ha finalizado después de cierto tiempo

El parámetro expected permite especificar el tipo de

excepción que debe lanzar el test para que sea exitoso

Comprobaciones

Pueden realizarse varias comprobaciones (aserciones) por método

La clase Assert proporciona un conjunto de métodos

estáticos para realizar comprobaciones

Para que una prueba se considere correcta tienen que cumplirse todas las aserciones especificadas

JUnit 4 (2)

Comprobaciones (cont): Ej: Assert.assertTrue(boolean)

Ej: Assert.assertEquals(Object, Object)

Compara utilizando el método equals (definido en Object)

Si no se redefine, el método equals de la clase Objectrealiza una comparación por referencia

Si para alguna clase se desease que la comparación fuese por contenido sería necesario redefinir el método equals y el método hashCode

La clase String y las correspondientes a los tipos básicos los tienen redefinidos para comparar por contenido

Cuando se trabaja con entidades manejadas por Hibernate, lo más sencillo es buscar una solución arquitectónica que no obligue a redefinir estos métodos (es decir, utilizar la igualdad referencial)

En el caso de los ejemplos de la asignatura, el código garantiza que durante la ejecución de un caso de uso nunca hay instancias duplicadas de entidades en memoria (el código escrito nunca crea instancias duplicadas, e Hibernate, una vez que carga una instancia en la sesión, siempre devuelva esa misma cada vez que se le pide)

JUnit 4 (y 3)

En general, cada método @Test se corresponde con un caso de prueba de un caso de uso, aunque a veces puede tener sentido implementar varios casos de prueba dentro de un mismo método @Test (si con ello se evita repetir código)

Para el caso de uso findAccount se han implementado dos casos de prueba Buscar una cuenta existente

testFindAccount

Buscar una cuenta inexistente testFindNonExistentAccount

Para el caso de uso withdrawFromAccount se han implementado tres casos de prueba Retirar dinero de una cuenta con balance suficiente

testWithdrawFromAccount

Retirar dinero de una cuenta inexistente testWithdrawFromNonExistentAccountAccount

Retirar dinero de una cuenta con balance insuficiente testWithdrawWithInsufficientBalance

JUnit: Inicialización y Borrado de Datos (1)

La idea de hacer las pruebas con un framework de pruebas automatizadas es que sean "pruebas automáticas repetibles"

Por tanto, cuando se ejecuten, deben hacer con anterioridad todo lo que sea necesario (crear datos necesarios) y con posterioridad restaurar el estado inicial (eliminar datos) para que puedan volver a ejecutarse más adelante

Es una buena práctica que la BD utilizada para los tests sea diferente a la BD utilizada en el entorno de ejecución, para que los datos de los tests y los datos “de ejecución” no entren en conflicto

Se utilizan las anotaciones @Before y @After para definir los

métodos a ejecutar antes y después de la ejecución de cada una de las pruebas de una clase

Se utilizan las anotaciones @BeforeClass y @AfterClass para

definir los métodos a ejecutar antes y después de la ejecución del conjunto de pruebas de una clase

JUnit: Inicialización y Borrado de Datos (2)

Cuando muchos casos de prueba necesitan crear los mismos datos en BD, la creación de esos datos es recomendable realizarla en el método anotado con @BeforeClass AccountServiceTest crea una cuenta de prueba en la BD, que muchos

casos de prueba asumirán que existe (e.g. testFindAccount,testWithdrawFromAccount ytestWithdrawWithInsufficientBalance), evitando así tener que crearla explícitamente

Cuando un caso de prueba necesita crear datos específicos (no comunes a un número significativo de casos de prueba) en BD, los crea directamente

El borrado de los datos globales a los casos de prueba hay que hacerlo en el método anotado con @AfterClass

Muchos casos de prueba necesitan acceder a los datos de prueba comunes, creados en @BeforeClass, para realizar comprobaciones Por ejemplo, el caso de prueba testFindAccount tiene que buscar una

cuenta cuyo identificador sea igual al de la cuenta que se insertó en la BD y comprobar que la cuenta obtenida es igual a la insertada

La cuenta creada en el método anotado con @BeforeClass podría cachearse en memoria

JUnit: Inicialización y Borrado de Datos (3)

La cuenta creada en el método anotado con @BeforeClass

podría cachearse en memoria (cont)

public class AccountServiceTest {

private static Account testAccount;

...

@BeforeClass

public static void populateDb() {

testAccount = << Create "testAccount" in DB >>

}

@Test

public void testFindAccount() throws InstanceNotFoundException {

Account account = accountService.find(testAccount.getAccountId());

assertEquals(testAccount, account);

}

...

}

JUnit: Inicialización y Borrado de Datos (4)

La cuenta creada en el método anotado con @BeforeClass

podría cachearse en memoria (cont)

Al ejecutar testFindAccount habría dos instancias en memoria que

hacen referencia a la misma cuenta (la referenciada por la variable global testAccount y la que está insertada en la sesión de Hibernate, referenciada por la variable local account)

Al ser dos instancias diferentes, la igualdad referencial no se cumple

La implementación por defecto de equals y hashCode no vale

Para evitar el problema hay dos soluciones

Redefinir equals y hashCode en Account

Problema: existen varias estrategias en Hibernate para redefinir equals y hashCode en entidades, cada una con sus ventajas y

desventajas

Más información en

https://www.hibernate.org/109.html

http://www.tic.udc.es/is-java/2008-2009/Tema3-5.pdf

No redefinir equals y hashCode, y garantizar que no hay

instancias duplicadas

JUnit: Inicialización y Borrado de Datos (5)

No redefinir equals y hashCode, y garantizar que no hay

instancias duplicadas (cont)

public class AccountServiceTest {

private static Long testAccountId;

...

@BeforeClass

public static void populateDb() {

<< Create "testAccount" in DB >>

testAccountId = ...;

}

@Test

public void testFindAccount() throws InstanceNotFoundException {

Account account =

accountService.findAccount(DbUtil.getTestAccountId()); // (1)

Account account2 = accountDao.find(DbUtil.getTestAccountId()); // (2)

assertEquals(account2, account);

}

...

}

JUnit: Inicialización y Borrado de Datos (y 6)

No redefinir equals y hashCode, y garantizar que no hay

instancias duplicadas (cont)

En el método anotado con @BeforeClass solamente se

guarda el identificador de la cuenta que se ha insertado

La cuenta es cacheada en la sesión de Hibernate tras la invocación (1)

Dado que la invocación (2) se ejecuta dentro de la misma sesión de Hibernate, se obtiene como resultado la instancia de Account cacheada en la sesión

La cuenta recuperada en (2) es el mismo objeto que el recuperado en (1) (asumiendo que las implementaciones de AccountService.find y AccountDao.find sean

correctas)

Por tanto, en este caso, la igualdad referencial (que es la que asumen las implementaciones por defecto de equals y hashCode) es suficiente

Es la aproximación que se ha seguido en los ejemplos de la asignatura, tanto en las pruebas, como en el resto del código

Uso de DAOs en casos de prueba

Como se comentó en la transparencia anterior, algunos casos de prueba necesitan crear datos específicos en BD

Además, algunos casos de prueba necesitan consultar datos de la BD para realizar comprobaciones

El criterio que se ha seguido para crear y recuperar datos desde los casos de prueba es el siguiente: Cuando se necesita hacer una operación para la que exista

un método en un servicio, se utiliza el método del servicio E.g. en testWithdrawFromAccount se utilizan findAccount y findAccountOperationsByDate de AccountService

Cuando no exista método en ningún servicio, entonces se utiliza directamente el método del DAO que corresponda E.g. en testFindAccount se utiliza find de AccountDao

Spring TestContext Framework

Proporciona un soporte genérico, basado en anotaciones, para la realización de pruebas de unidad o de integración de la capa modelo, independiente del framework concreto de pruebas utilizado

Además de la infraestructura genérica para la realización de pruebas, proporciona el soporte de integración con JUnit y TestNG

es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest

import static es.udc.pojo.minibank.model.util.GlobalNames.SPRING_CONFIG_FILE;

import static es.udc.pojo.minibank.test.util.GlobalNames.SPRING_CONFIG_TEST_FILE;

@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration(locations = { SPRING_CONFIG_FILE,

SPRING_CONFIG_TEST_FILE })

@Transactional

public class AccountServiceTest {

...

@Autowired

private AccountService accountService;

...

}

GlobalNames.java

package es.udc.pojo.minibank.model.util;

public class GlobalNames {

public static final String SPRING_CONFIG_FILE =

"classpath:/pojo-minibank-spring-config.xml";

private GlobalNames () {}

}

package es.udc.pojo.minibank.test.util;

public final class GlobalNames {

public static final String SPRING_CONFIG_TEST_FILE =

"classpath:/pojo-minibank-spring-config-test.xml";

private GlobalNames () {}

}

pojo-minibank-spring-config.xml

<!-- Enable usage of @Autowired. -->

<context:annotation-config/>

<!-- Enable component scanning for defining beans with annotations. -->

<context:component-scan base-package="es.udc.pojo.minibank.model"/>

<!-- Data source. -->

<bean id="dataSource"

class="org.springframework.jndi.JndiObjectFactoryBean"

p:jndiName="jdbc/pojo-examples-ds"

p:resourceRef="true" />

<!-- Hibernate Session Factory -->

<bean id="sessionFactory"

class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"

p:dataSource-ref="dataSource"

p:configLocation="classpath:/pojo-minibank-hibernate-config.xml"/>

pojo-minibank-spring-config.xml (y 2)

<!-- Transaction manager for a single Hibernate SessionFactory. -->

<bean id="transactionManager"

class="org.springframework.orm.hibernate3.HibernateTransactionManager"

p:sessionFactory-ref="sessionFactory" />

<!-- Enable the configuration of transactional behavior based on annotations. -->

<tx:annotation-driven transaction-manager="transactionManager" />

pojo-minibank-spring-config-test.xml

<!-- Data source. -->

<bean id="dataSource"

class="org.springframework.jdbc.datasource.SingleConnectionDataSource"

p:driverClassName="com.mysql.jdbc.Driver"

p:url="jdbc:mysql://localhost/pojotest"

p:username="pojo"

p:password="pojo"

p:suppressClose="true" />

Configuración (1)

Para que una clase de pruebas tenga acceso al contenedor (ApplicationContext) debe utilizar la anotación @ContextConfiguration a nivel de

clase

Por defecto se genera una localización para el fichero que contiene los metadatos de configuración (a partir de los cuales se crea el contenedor) basada en el nombre de la clase de test E.g. si la clase se llama com.example.MyTest se cargará

la configuración de "classpath:/com/example/MyTest-context.xml"

Configuración (y 2)

A través del atributo locations es posible especificar uno o varios ficheros de configuración de Spring Las pruebas usan dos ficheros de configuración:

el de src/main/resources (el visto en el apartado 3.4)

el de src/test/resources (específico para tests)

De esta forma el fichero de configuración para tests sólo tiene que redefinir o añadir los beans que precisen las pruebas (se evita duplicar información en los ficheros de configuración de Spring) Los beans definidos en un fichero de configuración

sobrescriben a los que tengan el mismo nombre en los ficheros de configuración especificados previamente en locations

En este caso, se redefine el bean dataSource para utilizar un DataSource de tipo SingleConnectionDataSource

Este tipo de DataSource devuelve siempre la misma conexión a la BD (fue explicado en el apartado 3.4)

La URL de conexión apunta a una BD diferente a la de ejecución

Inyección de dependencias

En las clases de test es posible realizar autoinyección de dependencias a través de la anotación @Autowired

Se ha inyectado el servicio AccountService sobre el que

se realizan las pruebas

Transacciones

Para habilitar el soporte de transacciones debe declararse un gestor de transacciones (un bean de tipo PlatformTransactionManager) en alguno

de los ficheros de configuración especificados a través de la anotación @ContextConfiguration

Por defecto se utiliza como gestor de transacciones el bean con nombre transactionManager

Si se desea utilizar otro, se puede especificar a través de la anotación @TransactionConfiguration

Se utiliza el gestor de transacciones declarado en el fichero de configuración de Spring ubicado en src/main/resources

Como se comentó con anterioridad, lo único que se redefine es el DataSource a ser utilizado

Integración con JUnit 4

Spring TestContext Framework se integra con JUnit 4 a través de un runner a medida

Anotando las clases de pruebas con @Runwith(SpringJUnit4ClassRunner.class),

es posible implementar tests de unidad o integración con JUnit 4 y al mismo tiempo beneficiarse del soporte que ofrece el Spring TestContext Framework para

Carga del contenedor (“application context”)

Inyección de dependencias en las clases de test

Ejecución transaccional de métodos de test

Etc.

es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest(recordatorio)

public class AccountServiceTest {

...

@BeforeClass

public static void populateDb() {

DbUtil.populateDb();

}

@AfterClass

public static void cleanDb() throws Exception {

DbUtil.cleanDb();

}

...

}

DbUtil.java (1)

public class DbUtil {

static {

ApplicationContext context = new ClassPathXmlApplicationContext(

new String[] {SPRING_CONFIG_FILE, SPRING_CONFIG_TEST_FILE});

transactionManager = (PlatformTransactionManager) context

.getBean("transactionManager");

accountDao = (AccountDao) context.getBean("accountDao");

}

private static Long testAccountId;

private static AccountDao accountDao;

private static PlatformTransactionManager transactionManager;

public static Long getTestAccountId() {

return testAccountId;

}

DbUtil.java (2)

public static void populateDb() throws Throwable {

/*

* Since this method is supposed to be called from a @BeforeClass

* method, it works directly with "TransactionManager", since

* @BeforeClass methods with Spring TestContext do not run in the

* context of a transaction (which is required for DAOs to work).

*/

TransactionStatus transactionStatus = transactionManager

.getTransaction(null);

Account testAccount = new Account(1, 10);

try {

accountDao.save(testAccount);

testAccountId = testAccount.getAccountId();

transactionManager.commit(transactionStatus);

} catch (Throwable e) {

transactionManager.rollback(transactionStatus);

throw e;

}

}

DbUtil.java (y 3)

public static void cleanDb() throws Throwable {

/*

* For the same reason as "populateDb" (with regard to @AfterClass

* methods), this method works directly with "TransactionManager".

*/

TransactionStatus transactionStatus = transactionManager

.getTransaction(null);

try {

accountDao.remove(testAccountId);

testAccountId = null;

transactionManager.commit(transactionStatus);

} catch (Throwable e) {

transactionManager.rollback(transactionStatus);

throw e;

}

}

}

Spring + JUnit: Inicialización y Borrado de Datos

Los métodos anotados con @BeforeClass y @AfterClass no se ejecutan dentro del contexto de

una transacción

Los datos se crean y borran utilizando los DAOs

Estos DAOs necesitan ejecutarse dentro del contexto de una transacción

Por tanto es necesario trabajar directamente con la API de transacciones de Spring (cuyas principales clases e interfaces se vieron en el apartado 3.4)

El gestor de transacciones está declarado en el archivo de configuración de Spring como un bean y es posible obtenerlo a través del método getBean después de instanciar el

contenedor

Los DAOs también se obtienen invocando el método getBean sobre el contenedor

Spring + JUnit: Transacciones

@Transactional en la clase de pruebas hace que Spring TestContext ejecute cada método @Test en una transacción y que haga un rollback al final Las modificaciones que haya hecho el caso de prueba a la BD (e.g.

testWithdrawFromAccount) se deshacen

El estado de la BD cuando se ejecuta el siguiente caso de prueba es el mismo que el que se estableció en @BeforeClass

Como ya se comentó con anterioridad, AccountServiceTestcachea el identificador de la cuenta de prueba creada en una variable global (DBUtil.getTestAccountId()) para que los casos de prueba que la requieran puedan recuperarla (e.g. testWithdrawFromAccount, testFindAccount)

Otra razón para no cachear como una variable global la cuenta de prueba, es que las posibles modificaciones que se hiciesen sobre ella no se desharían automáticamente al acabar la ejecución del caso de prueba El rollback que hace Spring TestContext después de la ejecución de

cada método @Test sólo afecta a la BD y no al estado de las variables globales