Tests parametrizados con JUnit 4

Una de las nuevas características de JUnit 4 es la posibilidad de correr tests parametrizados. Los tests parametrizados son una forma de escribir un test genérico y poder correrlo con juegos de datos diferentes.

Creando tests parametrizados

Para crear un test parametrizado con JUnit, necesitamos seguir los siguientes pasos:

  • Crear una clase de test anotada con @RunWith(Parameterized.class). El nombre completo del runner es org.junit.runners.Parameterized.
  • Agregar miembros de la clase que representen los parámetros de los tests.  Estos miembros no necesitan tener getters y setters, y pueden ser miembros privados.
  • Crear un constructor común de la clase, que reciba parámetros del tipo de los miembros y les asigne sus valores.
  • Crear un método estático que retorne una java.util.Collection.  Este
    método deberá estar anotado con @Parameters, y será el que define el juego de datos a utilizar.  Si los juegos de datos tienen más de una variable, deberá retornar una colección de arrays. Si es necesario -generalmente lo es-, podría contener además el resultado esperado para cada juego de datos de entrada.
  • Escribir un método anotado con @Test, en el cual se usen los miembros de la clase antes definidos.

Ejecución de tests parametrizados

La ejecución de un test parametrizado se realiza de forma idéntica a un test normal.

Al ejecutar el test, JUnit invocará al método anotado con @Parameters, construirá la clase de test tantas veces como juegos de datos haya, invocando cada vez al constructor con un juego diferente de datos. En cada una de esas instancias del tests ejecutará el test de forma normal, invocando -si existen- a los métodos anotados con @BeforeClass, @AfterClass, @Before, @After y al test en si. En resumen, la ejecución será:

  • @Parameters
  • @BeforeClass
  • @Before
  • @Test (juego de datos 1)
  • @After
  • @Before
  • @Test (juego de datos 2)
  • @After
  • @Before
  • @Test (juego de datos N)
  • @After
  • @AfterClass

Ejemplo

A modo de ejemplo imaginemos que tenemos una clase com.josearrarte.DateFormatProvider con un método getDateFormatFor(int operationType) y que, dependiendo del valor del operationType recibido, retorna un java.lang.Stringcon diferentes formatos de fecha («MMYY», «YYMM» o «MMYYYY»).

El código completo del ejemplo está disponible para bajar aquí.

<br />
package com.josearrarte;</p>
<p>public class DateFormatProvider {</p>
<p>	public<br />
static final int OPERATION_TYPE_1 = 1;</p>
<p>	public static final int OPERATION_TYPE_2 = 2;</p>
<p>	public static final int OPERATION_TYPE_3 = 3;</p>
<p>	public String getDateFormatFor(int operationType)<br />
		throws IllegalArgumentException {</p>
<p>		switch (operationType) {<br />
			case OPERATION_TYPE_1: {<br />
				return &quot;MMYY&quot;;<br />
			}<br />
			case OPERATION_TYPE_2: {<br />
				return &quot;YYMM&quot;;<br />
			}<br />
			case OPERATION_TYPE_3: {<br />
				return &quot;YYYYMM&quot;;<br />
			}<br />
			default: {<br />
				throw new IllegalArgumentException(&quot;Unknown operation type&quot;);<br />
			}<br />
		}<br />
	}<br />
}<br />

Podríamos generar una serie de tests parametrizados para los flujos básicos de ejecución y otro test para le caso que operationType no sea 1, 2 o 3. La clase de test podría ser como la siguiente;

<br />
package com.josearrarte;</p>
<p>import static org.junit.Assert.assertEquals;</p>
<p>import java.util.Arrays;<br />
import java.util.Collection;</p>
<p>import org.junit.Test;<br />
import org.junit.runner.RunWith;<br />
import org.junit.runners.<br />
Parameterized;<br />
import org.junit.runners.Parameterized.Parameters;</p>
<p>@RunWith(Parameterized.class)<br />
public class ParametrizedJunitTest {</p>
<p>	private int operationType;<br />
	private String dateFormat;</p>
<p>	public ParametrizedJunitTest(int operationType, String dateFormat) {<br />
		super();<br />
		this.operationType = operationType;<br />
		this.dateFormat = dateFormat;<br />
	}</p>
<p>	@Parameters<br />
	public static Collection operationTypeValues() {</p>
<p>		return Arrays.asList(new Object[][] {<br />
				{ DateFormatProvider.OPERATION_TYPE_1, &quot;MMYY&quot; },<br />
				{ DateFormatProvider.OPERATION_TYPE_2, &quot;YYMM&quot; },<br />
				{ DateFormatProvider.OPERATION_TYPE_3, &quot;YYYYMM&quot; }<br />
		});<br />
	}</p>
<p>	@Test<br />
	public void verifyDateFormats() throws Exception {<br />
		DateFormatProvider provider = new DateFormatProvider();</p>
<p>		String result = provider.getDateFormatFor(operationType);<br />
		assertEquals(dateFormat, result);<br />
	}</p>
<p>	@Test(expected=IllegalArgumentException.class)<br />
	public void verifyDateFormats_<br />
ShouldThrowIllegalArgumentException()<br />
		throws Exception {</p>
<p>		DateFormatProvider provider = new DateFormatProvider();</p>
<p>		provider.getDateFormatFor(-1);<br />
	}<br />
}<br />

Dejo planteado un problema que no supe resolver con JUnit. Si escribo un test parametrizado y otro que no es parametrizado en una misma clase, JUnit correrá a ambos como si fueran parametrizados, tantas veces como el tamaño de la colección de los datos. No encontré forma de configurar JUnit para que uno de los métodos (verifyDateFormats_ShouldThrowIllegalArgumentException() en el ejemplo) se ejecute como un test común y corriente. En éste post muestro cómo se puede resolver este tema utilizando TestNG en lugar de JUnit 4.

En resumen

Los tests parametrizados nos simplifican la tarea de escribir varios tests cuando su implementación es similar entre ellas.
Generalmente aporta claridad al código de la clase de test, ya que no se escribe tanto código repetido.

Sin embargo, si utilizamos JUnit debemos escribir los tests parametrizados en una clase separada de los tests comunes, ya que no es posible decirle a JUnit la forma de ejecutar un método de test y otro. Si no queremos tener este problema podemos utilizar TestNG, otro framework de testing unitario. En éste post muestro detalles de cómo hacerlo.

Etiquetado , . Bookmark the permalink.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *