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

Anuncio
3.5 Implementación de Pruebas de
Integración con Spring y JUnit
Índice


Código de pruebas
JUnit



Spring TestContext Framework





JUnit 4
Inicialización y borrado de datos
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



Con Maven el código de pruebas se ubica en el
directorio src/test (que tiene una estructura de
directorios paralela al directorio src/main)
En el subsistema 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
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
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();
}
@Test
public void testCreateAccount() throws InstanceNotFoundException {
Account account = accountService.createAccount(new Account(1, 10));
Account account2 = accountService.findAccount(account.getAccountId());
assertEquals(account, account2);
}
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;
if (add) {
newBalance = DbUtil.getTestAccount().getBalance() + amount;
} else {
newBalance = DbUtil.getTestAccount().getBalance() - amount;
}
startDate = Calendar.getInstance();
es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (3)
if (add) {
accountService.addToAccount(
DbUtil.getTestAccount().getAccountId(), amount);
} else {
accountService.withdrawFromAccount(
DbUtil.getTestAccount().getAccountId(), amount);
}
endDate = Calendar.getInstance();
/* Check new balance. */
Account account = accountService.
findAccount(DbUtil.getTestAccount().getAccountId());
assertTrue(newBalance == account.getBalance());
es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (4)
/* Check account operation. */
List<AccountOperationInfo> accountOperations =
accountService.findAccountOperationsByDate(
DbUtil.getTestAccount().getAccountId(),
startDate, endDate, 0, 2);
assertTrue(accountOperations.size() == 1);
AccountOperationInfo accountOperationInfo = accountOperations.get(0);
if (add) {
assertEquals(AccountOperation.Type.ADD,
accountOperationInfo.getType());
} else {
assertEquals(AccountOperation.Type.WITHDRAW,
accountOperationInfo.getType());
}
assertTrue(amount == accountOperationInfo.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();
try {
accountService.withdrawFromAccount(
DbUtil.getTestAccount().getAccountId(),
DbUtil.getTestAccount().getBalance() + 1);
} catch (InsufficientBalanceException e) {
exceptionCatched = true;
}
endDate = Calendar.getInstance();
assertTrue(exceptionCatched);
es.udc.pojo.minibank.test.model.accountservice.AccountServiceTest (7)
/* Check balance has not been modified. */
Account account = accountService.findAccount(
DbUtil.getTestAccount().getAccountId());
assertTrue( account.getBalance() == DbUtil.getTestAccount().getBalance());
/* Check account operation has not been registered. */
List<AccountOperationInfo> accountOperations =
accountService.findAccountOperationsByDate(
DbUtil.getTestAccount().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.assertEquals(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
Para las clases para las que se desee que la comparación sea
por contenido es necesario redefinir el método equals


La clase String y las correspondientes a los tipos básicos lo
tienen redefinido para comparar por contenido
En la clase Account se ha redefinido el método equals para
poder comparar instancias utilizando este método
JUnit 4 (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 withdrawFromAccount se han
implementado tres casos de prueba

Retirar dinero de una cuenta con balance suficiente


Retirar dinero de una cuenta inexistente


testWithdrawFromAccount
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"
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 con posterioridad


Utilizan anotaciones @Before y @After para definir los
métodos a ejecutar antes y después de la ejecución de cada
prueba
Utilizan 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 (y 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 (e.g. testWithdrawFromAccount y
testWithdrawWithInsufficientBalance) asumirán que
existe, evitando así tener que crearla explícitamente
La cuenta de prueba creada se almacena en una variable global
(DBUtil.getTestAccount()) para evitar tener que leerla de BD
en ciertos casos de prueba (e.g. testWithdrawFromAccount y
testWithdrawWithInsufficientBalance)
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
es.udc.pojo.minibank.test.model.account.Account
public class Account {
...
@Override
public int hashCode() {
return userId == null ? 0 : userId.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof Account))
return false;
final Account other = (Account) obj;
es.udc.pojo.minibank.test.model.account.Account
if (accountId == null) {
if (other.getAccountId() != null)
return false;
} else if (!accountId.equals(other.getAccountId()))
return false;
if (Double.doubleToLongBits(balance) !=
Double.doubleToLongBits(other.getBalance()))
return false;
if (userId == null) {
if (other.getUserId() != null)
return false;
} else if (!userId.equals(other.getUserId()))
return false;
if (version != other.getVersion())
return false;
return true;
}
}
Redefinición del método equals (1)

Cuando los objetos son entidades manejadas por
Hibernate es necesario tener en cuenta que el objeto
que recibe el método equals puede ser un proxy

Para ver si el objeto es de la misma clase que la instancia
con la que se está comparando es necesario utilizar el
operador instanceof en lugar del método getClass


El método getClass devolvería la clase del proxy
En vez de acceder a las propiedades directamente es
necesario acceder a ellas a través de los métodos get, ya
que en caso de ser un proxy las propiedades podrían estar
sin inicializar


Al llamar a cualquier método get las propiedades se inicializan
Nótese que para el caso del objeto sobre el que se está
invocando el equals sí es seguro acceder directamente a las
propiedades, porque en caso de ser un proxy se habrán
inicializado al realizar la llamada al método
Redefinición del método equals (2)


El método equals de la clase Account es el que
autogenera Eclipse modificado para tener en cuenta
los aspectos comentados en la transparencia anterior
Cuando los objetos son entidades manejadas por
Hibernate que tienen relaciones con otras entidades,
por escalabilidad, no se deben comparar las
propiedades que mantienen las relaciones


Si se comparasen se podrían cargar en memoria muchos
objetos
Por ejemplo en AccountOperation, en la comparación no
se tiene en cuenta la propiedad de tipo Account
Redefinición del método hashCode (1)

En Java para poder tratar objetos dentro de colecciones
indexadas es necesario que si el método equals indica que dos
objetos son iguales, entonces el método hashCode devuelva el
mismo valor para ambos




No se requiere lo contrario (si hashCode devuelve lo mismo no
implica que equals deba devolver true)
Es deseable, por motivos de eficiencia, maximizar la
probabilidad de que si equals indica que dos objetos no son
iguales hashCode devuelva un valor distinto
Una vez que un objeto ha sido insertado en una colección
indexada su hashCode no debe variar mientras el objeto
permanezca en la colección
La implementación de hashCode en la clase Object tiene en
cuenta la igualdad referencial (solamente asegura que devuelve
el mismo valor para la misma instancia de una clase)

Por tanto, si se redefine el método equals para que no realice
igualdad por referencia, es necesario redefinir también el método
hashCode
Redefinición del método hashCode (2)

Cuando los objetos son entidades manejadas por
Hibernate cuyas claves son autogeneradas, hay que
tener en cuenta que el objeto puede insertarse en
una colección cuando aún no tiene valor para esa
clave


Por tanto hashCode no debe tener en cuenta la clave de la
entidad a la hora de generar el valor devuelto (ya que con
posterioridad la clave pasará a tener valor)
¿Cómo generarlo entonces?


Este es un tema no exento de controversia en la comunidad
de Hibernate
En los ejemplos de la asignatura se ha optado por una
aproximación segura pero no siempre la más eficiente

Se genera en función de propiedades del objeto que sean
inmutables y cuyos valores lo identifiquen lo más unívocamente
posible
Redefinición del método hashCode (3)

¿Cómo generarlo entonces?

En los ejemplos de la asignatura se ha optado por una
aproximación segura pero no siempre la más eficiente (cont)

Si el objeto no tiene propiedades inmutables se genera en
función de las que sea menos probable que vayan a variar


En este caso hay que tener en cuenta que una vez insertado el
objeto en una colección, las propiedades de las que depende el
cálculo del hashCode no deben variar mientras permanezca el
objeto en la colección
En Account se ha generado en función de la
propiedad userId y en AccountOperation en
función de la propiedad date

En caso de tener valor nulo la propiedad, se devuelve un
valor constante
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
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { GlobalNames.SPRING_CONFIG_FILE_LOCATION })
@Transactional
public class AccountServiceTest {
...
private AccountService accountService;
...
@Autowired
public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}
...
}
GlobalNames.java
public final class GlobalNames {
public static final String SPRING_CONFIG_FILE_LOCATION =
"classpath:/pojo-minibank-spring-config.xml";
private GlobalNames () {}
}
Beans de la capa modelo de MiniBank (pojo-minibank-spring-config.xml)
<!-- DAOs. -->
<bean id="accountDao"
class="es.udc.pojo.minibank.model.account.AccountDaoHibernate"
p:sessionFactory-ref="sessionFactory" />
<bean id="accountOperationDao"
class=
"es.udc.pojo.minibank.model.accountoperation.AccountOperationDaoHibernate"
p:sessionFactory-ref="sessionFactory" />
<!-- Service layer. -->
<bean id="accountService"
class="es.udc.pojo.minibank.model.accountservice.AccountServiceImpl"
p:accountDao-ref="accountDao"
p:accountOperationDao-ref="accountOperationDao" />
Configuración


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"
A través del atributo locations es posible
especificar localizaciones alternativas del fichero
Inyección de dependencias


En las clases de test es posible realizar
“autoinyección” de dependencias a través de
anotaciones
Anotación @Autowired


Se anota el método set correspondiente a la propiedad que
se desea autoinyectar
La autoinyección se realiza por tipo


Se busca un bean en el fichero de configuración que coincida
con el tipo de la propiedad
También es posible indicar que la autoinyección se
realice por nombre utilizando la anotación
@Resource

Necesario si hay más de un bean con el mismo tipo que la
propiedad que se desea inyectar
Configuración de transacciones (pojo-minibank-spring-config.xml)
<!-- For translating native persistence exceptions to Spring's DataAccessException
hierarchy. -->
<bean
class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
<!-- Data source. -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="${jdbcDriver.className}"
p:url="${dataSource.url}" p:username="${dataSource.user}"
p:password="${dataSource.password}" />
<!-- Hibernate Session Factory -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
p:dataSource-ref="dataSource"
p:configLocation="classpath:pojo-minibank-hibernate.cfg.xml"/>
<!-- 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" />
Transacciones

Para habilitar el soporte de transacciones debe
declararse un gestor de transacciones (un bean de
tipo PlatformTransactionManager) en el
fichero de configuración especificado 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 un DataSource definido directamente sobre
un driver JDBC
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.
DbUtil.java (1)
public class DbUtil {
static {
ApplicationContext context = new ClassPathXmlApplicationContext(
GlobalNames.SPRING_CONFIG_FILE_LOCATION);
transactionManager = (PlatformTransactionManager)
context.getBean("transactionManager");
accountDao = (AccountDao) context.getBean("accountDao");
}
private static Account testAccount;
private static AccountDao accountDao;
private static PlatformTransactionManager transactionManager;
/**
* IMPORTANT: the returned object is a global object. In consequence, it
* must not be modified, since it can affect other tests.
*/
public static Account getTestAccount() {
return testAccount;
}
DbUtil.java (2)
public static void populateDb() {
/*
* 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);
testAccount = new Account(1, 10);
try {
accountDao.create(testAccount);
transactionManager.commit(transactionStatus);
} catch (Throwable e) {
transactionManager.rollback(transactionStatus);
throw e;
}
}
DbUtil.java (y 3)
public static void cleanDb() throws Exception {
/*
* 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(testAccount.getAccountId());
testAccount = 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 permite que Spring
TestContext ejecute cada método @Test en una transacción y
que hace un rollback al final



Las modificaciones que haya hecho el caso de prueba (e.g.
testWithdrawFromAccount) a la BD se deshacen
El estado de la BD es el mismo que el que se estableció en
@BeforeClass cuando se ejecuta el siguiente caso de prueba
Como ya se comentó con anterioridad, AccountServiceTest
cachea la cuenta de prueba creada en una variable global
(DBUtil.getTestAccount()) para evitar tener que leerla de
BD en ciertos casos de prueba (e.g.
testWithdrawFromAccount)

Esta variable global no debe ser modificada, dado que ello afectaría
a la ejecución de los siguientes casos 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)
Descargar