Los tests parametrizados nos pueden facilitar escribir tests donde queremos repetir un mismo test para varios conjuntos de entradas, y comprobar que para cada conjunto de entradas se devuelve la misma salida.
Por ejemplo, imaginemos que estamos de nuevo enfrascados en la creación de una calculadora y queremos probar el método de suma con distintos parámetros. Los tests parametrizados nos permiten crear rápidamente una tabla de valores, y dejar que JUnit repita el test para cada una de las filas de nuestra tabla.
param 1 param 2 return 3 2 5 4 6 10 2 2 4Si bien esto es algo que podríamos hacer a mano, por ejemplo, repitiendo el aserto:
assertEquals(5, sum(3, 2));
assertEquals(10, sum(4, 6));
assertEquals(4, sum(2, 2));
Se nos ocurren casos donde no sea comodo hacer esto, sobre todo si es más complicado de escribir el aserto porque hay que instanciar más clases colaboradoras. En este caso, los tests parametrizados pueden venir al rescate.
Cómo marcar un test como parametrizado
Para escribir un test parametrizado, tendremos que marcar toda la clase con la siguiente anotación:
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class AddCalculatorTest {
}
La anotación RunWith es una anotación especial que sirve para indicar que queremos utilizar una clase runner especial para lanzar el test. Los runners son las clases expertas que se ocupan de llamar a cada uno de los métodos de un test de JUnit, por lo que si cambiamos el runner, seguramente alteremos la forma en la que JUnit ejecuta nuestro test case. En este caso, Parameterized es un tipo de runner que se ocupa de los tests parametrizados.
Cómo generar la tabla de casos de un test parametrizado
Cuando usamos una clase parametrizada, tenemos que declarar un método estático extra que tiene que estar anotado con @Parameter.Paremeters (o simplemente como @Parameters si importamos la anotación org.junit.runners.Parameterized.Parameters). Este método tiene que devolver un objeto de tipo Iterable, que exponga arrays.
Mientras el iterable se pueda recorrer, se irá extrayendo cada uno de los valores que vaya generando. Cada una de estas extracciones se corresponderá con una ejecución más del test case usando un nuevo conjunto de parámetros en función de lo que devuelva el iterable.
El iterable devuelve un array de objetos. Cada elemento de este array se corresponde con una columna de la tabla. Por supuesto, los arrays deben tener el mismo tamaño para que todo funcione. Cada elemento se proporcionará como parámetro en el constructor.
Por ejemplo, para crear un método de parámetros para la tabla que he puesto antes, podríamos hacerlo así:
@Parameterized.Parameters
public static Collection<Object[]> valores() {
return Arrays.asList(new Object[][] {
{ 3, 2, 5 },
{ 4, 6, 10 },
{ 2, 2, 4 }
});
}
La función valores() está devolviendo una colección de tres elementos, por lo que el runner ejecutará nuestro test tres veces. En la primera vez, el conjunto de valores a usar será {3, 2, 5}, para la segunda será {4, 6, 10} y para la tercera será {2, 2, 4}.
Cómo se instancia un test parametrizado
Los tests parametrizados son un poco especiales, en el sentido de que son tests donde sí se va a usar el constructor de la clase. Normalmente en un test de JUnit no usaríamos el constructor, y si queremos inicializar algo cuando arranca el test, lo dejaríamos para una anotación como la Before o la BeforeClass. Sin embargo, los tests parametrizados son especiales en el sentido de que sí lo usan.
Para cada una de las filas de nuestra tabla de tests (la que se devuelve en el método anotado como @Parameter.Parameters), se llamará al constructor, pasándole los elementos del array en orden como parámetros primero, segundo, tercero… Por supuesto, es importante que el número de elementos de cada array de la tabla sea el mismo, como ya dije, pero también es importante que el número de elementos de cada array coincida en primer lugar con el número de parámetros admitidos por el constructor de la clase de test.
Veamos el caso completo:
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class AddCalculatorTest {
@Parameterized.Parameters
public static Collection<Object[]> valores() {
return Arrays.asList(new Object[][] {
{ 3, 2, 5 },
{ 4, 6, 10 },
{ 2, 2, 4 }
});
}
public AddCalculatorTest(int a, int b, int result) {
this.a = a;
this.b = b;
this.result = result;
}
private int a, b, result;
}
En este caso, AddCalculatorTest tiene un constructor de tres parámetros: a, b y test. Los tres son de tipo int. Se espera, por lo tanto, que la función que genera los parámetros devuelva un iterador de arrays de 3 enteros. Para este ejemplo, es así.
Se invocará tres veces AddCalculatorTest, pasándole cada una de las filas de la tabla como parámetros de forma ordenada, de izquierda a derecha. Así que, en definitiva, se instanciará la clase tres veces con los siguientes parámetros:
AddCalculatorTest(a = 3, b = 2, result = 5)AddCalculatorTest(a = 4, b = 6, result = 10AddCalculatorTest(a = 2, b = 2, result = 4)
Ahora podemos usar esos parámetros capturados, si los guardamos en atributos de clase, para nuestros tests, por lo que el test final podría tener este aspecto:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class AddCalculatorTest {
@Parameterized.Parameters
public static Collection<Object[]> valores() {
return Arrays.asList(new Object[][] {
{ 3, 2, 5 },
{ 4, 6, 10 },
{ 2, 2, 4 }
});
}
public AddCalculatorTest(int a, int b, int result) {
this.a = a;
this.b = b;
this.result = result;
}
private int a, b, result;
@Test
public void testSum() {
Calculadora calc = new Calculadora();
assertEquals(this.result, calc.sumar(this.a, this.b));
}
}
En este caso, los datos que le entran a la calculadora y al aserto vienen de los atributos de clase.