Í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
Top Related