JUnit test runners

0
17145

JUnit test runners.

0. Índice de contenidos.


1. Introducción

Cuando anotamos un test de JUnit con la anotación @RunWith o extendemos de una clase con dicha anotación, JUnit
invoca a la clase referenciada en la anotación para lanzar los tests en vez de al lanzador por defecto. El lanzador
por defecto es BlockJUnit4ClassRunner.

En este tutorial vamos a examinar los distintos runners que provee JUnit, out of the box, runners de terceros y veremos
cómo crear nuestros propios runners.


2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.4 GHz Intel Core i7, 8GB DDR3 SDRAM).
  • Sistema Operativo: Mac OS X Lion 10.7.4
  • JUnit 4.10


3. JUnit Runners.

Podemos encontrar los siguientes runners dentro de la distribución de JUnit:


3.1. Suite.

Permite crear manualmente una suite que contenga tests de distintas clases. Las clases que forman parte de la suite
se incluyen en la anotación @Suite, que no solo indica las clases que se incluirán sino también el orden de ejecución
de los tests.

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({
  LoginTest.class,
  MenuTest.class,
  LogoutTest.class
})

public class MyTestSuite {
  // void
}


3.2. Parametrized.

Cuando anotamos una clase con @RunWith(Parametrized.class) se crearán tantas instancias y se ejecutarán tantas veces los
métodos de tests como parámetros devolvamos en el método anotado @Parameters

@RunWith(Parameterized.class)
public class FibonacciTest {
    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {
                
                 { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 },{ 6, 8 }  
           });
    }

    private int fInput;

    private int fExpected;

    public FibonacciTest(int input, int expected) {
        fInput= input;
        fExpected= expected;
    }

    @Test
    public void test() {
        assertEquals(fExpected, Fibonacci.compute(fInput));
    }
}

El método de test se invocará tantas veces como elementos hay dentro del array y, en la invocación tendremos los
valores de los mismos poblados en las variables de clase, si los inicializamos en el constructor de la misma.


3.3. Categories.

Las categorías sirven como cualificadores de nuestros tests de modo tal que nos permiten incluir o excluir en una suite
aquellos tests anotados con ciertos cualificadores.


public interface FastTests { }

public interface SlowTests { }

public class A {
  @Test
  public void a() {
    fail();
  }

  @Category(SlowTests.class)
  @Test
  public void b() {
  }
}

@Category({SlowTests.class, FastTests.class})
public class B {
  @Test
  public void c() {

  }
}

@RunWith(Categories.class)
@IncludeCategory(SlowTests.class)
@SuiteClasses( { A.class, B.class }) 
public class SlowTestSuite {
  // Ejecuta el test A.b y el B.c, pero no el A.a porque no está anotado con @Category(SlowTests.class)
}

@RunWith(Categories.class)
@IncludeCategory(SlowTests.class)
@ExcludeCategory(FastTests.class)
@SuiteClasses( { A.class, B.class })
public class SlowTestSuite {
  // Ejecuta el A.b, pero no el A.a ni el B.c, porque no complen la codición de exclusión
}


4. Runners de terceros.

La mayoría de frameworks que implementan patrones creacionales de objetos, basándose en JUnit, proporcionan un
runner que permite disponer de dicho patrón dentro del entorno de la ejecución de tests de JUnit. A continuación
mostramos unos ejemplos aunque no son los únicos.


4.1. Spring.

Con el soporte del runner SpringJUnit4ClassRunner y la anotación @ContextConfiguration que indica los ficheros de configuración
de Spring que configuran el contexto en el ámbito de este test.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext-test.xml" })


4.2. Arquilian.

Con el soporte del runner Arquilian.class y la anotación @Deployment que devuelve el paquete que contendrá
las clases que formarán parte del framework con el que correrá el test dentro del contenedor (CDI, ODGI,…).

@RunWith(Arquillian.class)
public class CalculadoraIntegrationTest
{
    @Deployment
    public static JavaArchive createdeployment() {
    


4.3. Mockito.

Con el soporte del runner MockitoJUnitRunner y la anotación @Mock, mockito se encarga de crear un mock para los objetos anotados.

@RunWith(MockitoJUnitRunner.class)  
public class SmallNotesApiTest {  
  
    @Mock  
    private SecurityContext securityContext;  


5. Nuestro propio runner.

Imagina que tenemos nuestro propio framework, que no se basa en ningún producto conocido o necesitamos
inicializar el contexto de acceso a base de datos que no descansa en ningún sistema de ORM estandar,
sino uno comercial que no tiene soporte para JUnit; podríamos dar soporte, desde arquitectura, creando un runner propio.

A continuación vamos a ver un ejemplo de cómo implementar nuestro propio runner para establecer un límite máximo
a la ejecución de un test unitario, de tal modo que un test fallará si no se ejecuta, por defecto, en menos de 500 milisegundos.

package com.autentia.osgi.tutorial;

import java.util.HashMap;
import java.util.Map;

import org.junit.Assert;
import org.junit.runner.Description;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;

public class TimeoutRunner extends BlockJUnit4ClassRunner {

	private static final long DEFAULT_TIMEOUT = 500;
	
	public TimeoutRunner(Class<?> clazz) throws InitializationError {
		super(clazz);
	}

	private Map<String,Long> methodInitialization = new HashMap<String, Long>();

	@Override
	public void run(RunNotifier notifier) {
		notifier.addListener(new RunListener() {

			@Override
			public void testStarted(Description description) throws Exception {
				final String methodName = description.getMethodName();
				methodInitialization.put(methodName, System.currentTimeMillis());
			}

			@Override
			public void testFinished(Description description) throws Exception {
				final String methodName = description.getMethodName();
				final long init = methodInitialization.get(methodName);
				final long timeSpent =  System.currentTimeMillis() - init;
				if ( timeSpent > DEFAULT_TIMEOUT){
					Assert.fail("Testing time-out, up than "+DEFAULT_TIMEOUT+" mils : " + methodName);
				}
			}

		});
		
		super.run(notifier);
	
	}
}

Ahora un test que hace uso del runner:

package com.autentia.osgi.tutorial;

import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(TimeoutRunner.class)
public class StringConcatTest 
{
 
    @Test
    public void usingStringBuilderIsFaster() {
        
        StringBuilder cadena = new StringBuilder();
        for (int i = 0; i 

Y el resultado es el siguiente:

A parte de hacer hincapié en la diferencia entre concatenar cadenas dentro de un bucle y usar StringBuilder, vemos como la ejecución del test se marca como fallida a causa del tiempo consumido en el segundo test.

6. Disclaimer sobre la funcionalidad del runner.

La funcionalidad del runner es lo de menos, se trataba de mostrar cómo implementar uno.

Podíamos haber conseguido nuestro objetivo también con el parámetro timeout de la propia anotación @Test a nivel de método sin necesidad de nuestro runner:


@Test(timeout=500)
public void testWithTimeout() {
  ...
}

o a nivel de clase con una regla @Rule

public class StringConcatTest {


    @Rule
    public Timeout globalTimeout = new Timeout(500);

    @Test
    public...
    

Volviendo a nuestro runner, y basándonos en la misma filosofía vamos a afinar un poco más permitiendo redefinir el límite de time-out por defecto a través de una anotación.

Para ello, lo primero es crearnos la interfaz de la anotación:

package com.autentia.osgi.tutorial;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeOut {
	public long value() default 500;
}

Que nos permitirá indicar a nivel de clase el timeOut de todos los tests que corren con nuestro runner:

@RunWith(TimeoutRunner.class)
@TimeOut(value=1000)
public class StringConcatTest 
{

Para obtener el valor de timeOut establecido en la anotación debemos refactorizar nuestro runner para acceder a la
anotación a nivel de clase:

package com.autentia.osgi.tutorial;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import junit.framework.Assert;

import org.junit.runner.Description;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;

public class TimeoutRunner extends BlockJUnit4ClassRunner {

	private static final long DEFAULT_TIMEOUT = 500;
	
	public TimeoutRunner(Class<?> clazz) throws InitializationError {
		super(clazz);
	}

	private Map<String,Long> methodInitialization = new HashMap<String, Long>();

	@Override
	public void run(final RunNotifier notifier) {
		
		notifier.addListener(new RunListener() {

			@Override
			public void testStarted(Description description) throws Exception {
				final String methodName = description.getMethodName();
				methodInitialization.put(methodName, System.currentTimeMillis());
			}

			@Override
			public void testFinished(Description description) throws Exception {
				final String methodName = description.getMethodName();
				final long init = methodInitialization.get(methodName);
				final long timeSpent =  System.currentTimeMillis() - init;
				final long timeOut = getTimeOut(description);
				if ( timeSpent > timeOut){
					Assert.fail("Testing time-out, up than "+timeOut+" mils : " + methodName);
				}
			}

			private long getTimeOut(Description description) {
				final List annotations = Arrays.asList(description.getTestClass().getAnnotations());
				for (Annotation annotation : annotations) {
					if (annotation instanceof TimeOut) {
						final TimeOut timeOut = (TimeOut) annotation;
						return timeOut.value();
					}
				}
				return DEFAULT_TIMEOUT;
			}

			
		});
		
		super.run(notifier);
	
	}
}

Aunque establezcamos el valor de timeOut en 1 segundo sigue sin pasar el test de la concatenación de cadenas 😉



7. Referencias.


8. Conclusiones.

Desde el punto de vista de arquitectura debemos proveer de las piezas de infraestructura necesarias no
solo para el entorno normal de ejecución y que estas sean lo más transparentes posibles al desarrollador,
sino también para el entorno de test.

Un saludo.

Jose

jmsanchez@autentia.com

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad