miércoles, 12 de mayo de 2010

Spring, JUnit4 y Maven

Esta vez vamos a ver como podemos crear nuestras pruebas unitarias mediante junit4.4 sean estas pruebas simples o transaccionales, veamos primero que es lo que vamos a probar:

Ejemplo 1; Una clase calculadora simple:


package pe.com.slcsccy.ejemplos.spring.junit4.service;
import java.math.BigDecimal;
import org.springframework.stereotype.Service;

@Service("calculadoraService")
public class CalculadoraServiceImpl implements CalculadoraService{

public BigDecimal dividir(BigDecimal operador1, BigDecimal operador2) {
return operador1.divide(operador2);
}

public BigDecimal multiplicar(BigDecimal operador1, BigDecimal operador2) {
return operador1.multiply(operador2);
}

public Long restar(Long operador1, Long operador2) {
return operador1 - operador2;
}

public Long sumar(Long operador1, Long operador2) {
return operador1 + operador2;
}

}

Y ahora veamos la clase de Test asociada a la calculadora:


package pe.com.slcsccy.ejemplos.spring.junit4.service;
import static junit.framework.Assert.assertEquals;
import java.math.BigDecimal;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
* Para que está clase ejecute test unitarios, debe de estar anotada con 'RunWith'
* cuyo valor es la clase SpringJUnit4ClassRunner, esta clase hereda de la clase Runner de junit4
* que declara un método abstracto run que Spring implementa.
* Para poder inyectar los beans de spring lo hacemos mediante la anotación
* 'ContextConfiguration' que espera la ruta o classpath donde se encuentran los contenedores
* de beans(*.xml)
* */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:/ejemplos-junit4-all.xml"})
public class CalculadoraServiceTest{


//Inyectamos el bean instanciado por Spring
@Autowired
private CalculadoraService calculadoraService;


//Creamos un log
private static Log logger = LogFactory.getLog("pe.com.slcsccy.ejemplos.spring.junit4.service.CalculadoraServiceTest");


//Los métodos anotados con @BeforeClass serán ejecutados una vez por
//cada instancia de esta clase al crearse la clase.
@BeforeClass
public static void inicializacionClass(){
logger.info("Inicialización de la clase de Test...!");
}

//Los métodos anotados con @AfterClass serán ejecutados una vez por
//cada instancia de esta clase al terminar la ejecución de todos los métodos de prueba.
@AfterClass
public static void finalizacionClass(){
logger.info("Finalización de la clase de Test...!");
}

//La anotación @Before pertenece a junit4.4 y le indica a la clase que el mètodo
//anotado se ejecute antes de la ejecución de cada método de prueba
@Before
public void inicializacion(){
logger.info("Inicialización de método de prueba ...!");
}

//Si en la clase de test hubiera varios métodos anotados con @Before
//entonces todos ellos se ejecutaran al iniciar cada método de prueba.
@Before
public void inicializacion2(){
logger.info("Inicialización de método de prueba 2...!");
}


//La anotación @After pertenece a junit4.4 y le indica a la clase que el método
//anotado se ejecute después de la ejecución de cada método de prueba
@After
public void finalizacion(){
logger.info("Finalización de método de prueba...!");
}

//Si en la clase de test hubiera varios métodos anotados con @After
//entonces todos ellos se ejecutaran al terminar cada método de prueba.
@After
public void finalizacion2(){
logger.info("Finalización de método de prueba 2...!");
}


//Método que realizará una prueba unitaria, ojo que no es necesario que
//el nombre del método empiece con test, simplemente que esté anotado con @Test
@Test
public void testSumar(){
logger.info("Inicio de test testSumar()");
Long operador1 = 1L;
Long operador2 = 2L;
Long resultado = calculadoraService.sumar(operador1, operador2);
assertEquals(3L, resultado.longValue());
logger.info("Fin de test testSumar()");
}

//Este tambien es un método de prueba que a diferencia del anterior no empieza con la
//palabra test
@Test
public void pruebaSumar(){
logger.info("Inicio de test pruebaSumar()");
Long operador1 = 2L;
Long operador2 = 3L;
Long resultado = calculadoraService.sumar(operador1, operador2);
assertEquals(5L, resultado.longValue());
logger.info("Fin de test pruebaSumar()");
}


//Esta prueba utiliza un parámetro de la anotación @Test que indica la excepción esperada
//por el método, si la excepción es lanzada entonces la prueba es correcta, si el método termina
//sin lanzar una excepción entonces la prueba falla; en el siguiente caso al dividir por cero
//el método lanza la excepción ArithmeticException que la prueba espera.
@Test(expected=ArithmeticException.class)
public void pruebaDivisionPorCero(){
logger.info("Inicio de test pruebaDivisionPorCero()");
BigDecimal operador1 = BigDecimal.TEN;
BigDecimal operador2 = BigDecimal.ZERO;
BigDecimal resultado = calculadoraService.dividir(operador1, operador2);
assertEquals(5L, resultado.longValue());
logger.info("Fin de test pruebaDivisionPorCero()");
}

//El otro parámetro de la anotación @Test es 'timeout' que indica que la prueba fallará
//si la ejecución del método se demora más del valor del parámetro 'timeout' en milisegundos
//en el ejemplo lo he colocado en 1000 milisegundos ó 1 segundo
@Test(timeout=1000)
public void pruebaMetodoLargo(){
logger.info("Inicio de test pruebaMetodoLargo()");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
logger.info("Hilo interrumpido en pruebaMetodoLargo()",e);
}
logger.info("Fin de test pruebaMetodoLargo()");
}

//Anotando un método de prueba con la anotación @Ignore le indicará a junit4 que dicho método
//no debe ser tomado en cuenta al momento de realizar las pruebas unitarias.
@Ignore
@Test(timeout=1000)
public void pruebaMetodoLargoIgnorado(){
logger.info("Inicio de test pruebaMetodoLargoIgnorado()");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.info("Hilo interrumpido en pruebaMetodoLargoIgnorado()",e);
}
logger.info("Fin de test pruebaMetodoLargoIgnorado()");
}

}


Ahora veamos una prueba más interesante, que tal si queremos probar operaciones con base de datos, veamos la siguiente prueba de registro de un empleado usando el esquema HR de oracle:

Ejemplo 2: Registro de empleados en una base de datos Oracle, primero veamos la clase que va a ser testeada:


package pe.com.slcsccy.ejemplos.spring.junit4.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import pe.com.slcsccy.ejemplos.spring.junit4.dominio.beans.Employees;
import pe.com.slcsccy.ejemplos.spring.junit4.model.dao.EmployeesDAO;

@Service("empleadoService")
@Transactional
public class EmpleadoServiceImpl implements EmpleadoService{

@Autowired
private EmployeesDAO employeeDao;

/**
* Recordar que la propagación REQUIRED es la que se usa por default
* la coloco para que sea más claro que si un método de prueba no inicia
* una transacción pues los métodos de esta clase que participan en la prueba
* sí iniciarán una transacción.
* */
@Override
@Transactional(propagation=Propagation.REQUIRED)
public void registrar(Employees empleado) {
employeeDao.insert(empleado);
}

@Override
@Transactional(readOnly=true)
public Employees buscar(Integer idEmpleado) {
return (Employees)employeeDao.selectByPrimaryKey(idEmpleado);
}
}


Ahora la clase de Test del servicio de registro de empleados


package pe.com.slcsccy.ejemplos.spring.junit4.service;

import java.util.Date;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.NotTransactional;
import org.springframework.test.annotation.Repeat;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;

import pe.com.slcsccy.ejemplos.spring.junit4.dominio.beans.Employees;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:/ejemplos-junit4-all.xml"})
@TransactionConfiguration(transactionManager="administradorTransaccional",defaultRollback=false)
public class EmpleadoServiceTest extends AbstractTransactionalJUnit4SpringContextTests {

@Autowired
private EmpleadoService empleadoService;

/**
* La anotación @Rollback la indicará a spring que los cambios que involucre el método de
* prueba no deben de comprometerse en forma permanente en la base de datos.
* Por otro lado la anotación @Repeat ejecutará tantas veces el método como el valor de su argumento.
* Debemos de tener en cuenta que en la ejecución del método de prueba se crea una nueva transacción y si
* la capa de servicios (empleadoService.*) crea otra transacción entonces la primera transacción se suspende
* y esta ultima transacción se comprometerá si se ejecuta satisfactoriamente, por lo que el método del servicio
* debería estar especificado de la siguiente manera:
* @Transactional(propagation=Propagation.REQUIRED) // esto es por default, y no como:
* @Transactional(propagation=Propagation.REQUIRES_NEW) // esto no tomaría en cuenta la anotación @Rollback
* public void registrar(Employees empleado) {...}
* */
@Test
@Rollback
@Repeat(5)
public void pruebaRegistroEmpleado(){
Employees empleado1 = new Employees();
empleado1.setEmployeeId(100001);
empleado1.setLastName("ccacique");
empleado1.setEmail("ccacique@sunat.gob.pe");
empleado1.setHireDate(new Date(System.nanoTime()));
empleado1.setJobId("IT_PROG");
empleadoService.registrar(empleado1);
Employees empleado2 = empleadoService.buscar(empleado1.getEmployeeId());
logger.info("Correo del empleado en base de datos: "+empleado2.getEmail());
}

/**
* La anotación @NotTransactional le indicará a spring que NO se debe de crear una transacción al iniciar
* la ejecución del método de prueba, por default los métodos de la clase que hereden
* de AbstractTransactionalJUnit4SpringContextTests son transaccionales.
* */
@Test
@NotTransactional
public void pruebaRegistroEmpleadoNoTransaccional(){
Employees empleado1 = new Employees();
empleado1.setEmployeeId(100002);
empleado1.setLastName("ccacique");
empleado1.setEmail("ccacique@elcacique.pe");
empleado1.setHireDate(new Date(System.nanoTime()));
empleado1.setJobId("IT_PROG");
//La anotación @NotTransactional indica que todo el código
//del método pruebaRegistroEmpleadoNoTransaccional
//no es transaccional, pero la capa de servicios(empleadoService.registrar(empleado1)) al tener
//su propio contexto transaccional inicia su propia transacción y compromete los cambios.
empleadoService.registrar(empleado1);
}
}


El proyecto utiliza el siguiente pom para resolver las dependencias del proyecto:



4.0.0
pe.com.slcsccy.ejemplos.spring
junit4
jar
0.0.1-SNAPSHOT
junit4
http://maven.apache.org


junit
junit
4.4
test


org.springframework
spring-test
2.5.6
test


com.oracle
ojdbc6
11.2.0.1.0


commons-dbcp
commons-dbcp
1.4


org.apache.ibatis
ibatis-sqlmap
2.3.0


org.springframework
spring
2.5.6


log4j
log4j
1.2.14





org.apache.maven.plugins
maven-surefire-plugin
2.4

false







Las dependencias del driver de oracle las he instalado a mano en mi repositorio ya que no son públicas. Las pruebas se ejecutan con un plugin de maven que se ejecuta en la fase de test: maven-surefire-plugin
El proyecto completo te lo puedes bajar desde aquí

No hay comentarios.:

Publicar un comentario

Es bueno comunicarnos, comenta!!.