Está en la página 1de 39

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 Object
realiza 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 y
testWithdrawWithInsufficientBalance), 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, AccountServiceTest
cachea 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

También podría gustarte