Parametized Test con JUnit 5 en ejemplos
La llegada de JUnit 5 trajo consigo algunas mejoras y novedades como los Parametized Test con JUnit 5, esta nueva funcionalidad nos va a permitir ejecutar un test múltiples veces con diferentes parámetros.
En este artículo nos vamos a centrar en el uso de los Parametized Test para realizar testing de nuestra aplicación, en este artículo anterior puedes ver algunos ejemplos más de testing con Spring Boot.
¿Qué es JUnit?
JUnit es un conjunto de bibliotecas que nos va a permitir realizar pruebas de nuestras aplicaciones Java.
JUnit 5 es la última generación de JUnit la cual trae un gran número de funcionalidades entre ellas el uso de Parametized Test.
Dependencia Maven para hacer uso de Parametized Test
La dependencia que usamos para añadir JUnit 5 es:
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <version>${version}</version> <scope>test</scope> </dependency>
si hacemos uso de Spring Boot ya viene la dependencia de JUnit 5
¿Para qué usar Parametized Test?
Los Parametized Test incorporado en JUnit 5 nos va a permitir con el simple uso de una anotación añadir diferentes valores para realizar pruebas sobre un método.
Por ejemplo tenemos un método que calcula si un número es par o impar y queremos con varios números, para ello haríamos uso de Parametized Test ya que nos permite pasar un rango de números.
Por ejemplo:
public class Calculate { public static boolean isEven(int number) { return number % 2 == 0; } }
Teniendo en cuenta el método anterior queremos calcular para diferentes números si es par o impar. Para los casos que queremos pasar un número limitado de valores por método podemos hacer uso de la anotación @ValueSource.
@ParameterizedTest @ValueSource(ints = {0, 2, 4, 6}) void given_4_even_numbers_should_return_true(int number) { assertTrue(Calculate.isEven(number)); }
El test que hemos ejecutado anteriormente (no es necesario que lleve @Test), ejecutaría de manera secuencial y en orden los valores que vienen indicados en @ValueSource.
Usos de Parametized Test con JUnit 5
En función de nuestras necesidades y cómo queramos realizar nuestros test y la carga de los diferentes valores, podemos realizarlo de diferentes maneras.
Vamos a realizar pruebas en el siguiente método:
Uso de Parametized Test con Null y Empty
Cuando estamos realizando pruebas y test de nuestros métodos es de vital importancia validar si nuestra aplicación va a funcionar de manera correcta con valores nulos y/o vacíos. Para poder realizar estas pruebas podemos pasar un valor null y vacío haciendo uso de anotaciones:
public class User { private static final String name = "noel"; public static String concat(String surname) { if (surname == null || surname.isEmpty()) { return name; } return name + " " + surname; } }
Verificar valor Null con Parametized Test
Vamos a hacer uso de la anotación @NullSource para pasar un valor null al método generado arriba.
En el caso en el que recibamos un null como parámetro devolveremos solo el nombre, en otro caso la concatenación.
@ParameterizedTest @NullSource void given_null_surname_then_return_name(String surname) { assertTrue(User.concat(surname).equalsIgnoreCase("noel")); }
Verificar valor empty con Parametized Test
Otra de las anotaciones que nos ofrece JUnit 5 junto con la funcionalidad de Parametized Test es @EmptySource, la cual va a pasar un valor vacío por parámetro. Si tenemos en cuenta el método concat creado anteriormente podríamos hacer un test de la siguiente manera:
@ParameterizedTest @EmptySource void given_empty_surname_then_return_name(String surname) { assertTrue(User.concat(surname).equalsIgnoreCase("noel")); }
El anterior test nos va a permitir pasar por parámetro un valor vacío para poder realizar pruebas.
Uso de empty y Null con una anotación con Parametized Test
En los dos puntos anteriores hemos visto dos anotaciones con la que podemos pasar null o empty, con la anotación @NullAndEmptySource podemos combinar ambos.
@ParameterizedTest @NullAndEmptySource void given_empty_surname_then_return_name(String surname) { assertTrue(User.concat(surname).equalsIgnoreCase("noel")); }
Uso de Parametized Test con @ValueSource
El primer ejemplo que hemos visto de Parametized Test veíamos como le podíamos pasar valores con la anotación @ValueSource, en los siguientes punto vamos a ver algún ejemplo más y los tipos de datos con los que se puede usar.
Hay que tener en cuenta que no podemos pasar valores null, para ello, como hemos visto en el punto anterior podemos hacer uso @NullSource.
Únicamente podemos hacer uso de los siguientes tipos de datos:
- short
- byte
- int
- long
- float
- double
- char
- java.lang.String
- java.lang.Class
Si hacemos un test en el que le pasamos vacío, un espacio y un apellido al método concat creado antes, podemos hacer algo así:
@ParameterizedTest @ValueSource(strings = {"", " ", "rodriguez"}) void given_values_then_return_ok(String surname) { assertNotNull(User.concat(surname)); }
CSV con Parametized Test
Una de las posibilidades que nos ofrece el uso de Parametized Test es el poder aplicar formatos CSV a nuestros test. De esta manera vamos a poder cargar gran cantidad de datos para realizar pruebas y asegurar el correcto funcionamiento de nuestras aplicaciones.
Para hacer uso de CSV en nuestros test podemos hacer uso de dos anotaciones direrentes:
- @CsvSource: Carga los datos a partir de literales pasados con la anotación.
- @CsvFileSource: Carga los datos a partir de un fichero.
Uso de @CsvSource en Parametized Test
Con el uso de @CsvSource vamos a pasar una clave valor separada por una «coma» como valor por defecto aunque podemos hacer uso de cualquir otro separador siempre y cuando lo indiquemos, vamos a ver como funciona.
Vamos a hacer un método en el que la suma de dos al primer parámetros es igual que al segundo parámetro.
public class Test{ public static int plusNumber(int number) { return number + 2; } }
@ParameterizedTest @CsvSource({"2,4", "6,8", "10,12"}) void given_a_number_when_plus_then_should_generate_the_expected(int input, int expected) { int value= Test.plusNumber(input); assertEquals(expected, value); }
Si queremos cambiar la coma por dos puntos, por ejemplo, podemos añadir el parámetro delimiter:
@ParameterizedTest @CsvSource(value = {"2:4", "6:8", "10:12"}, delimiter = ':') void given_a_number_when_plus_then_should_generate_the_expected(int input, int expected) { int value= Test.plusNumber(input); assertEquals(expected, value); }
Como podemos ver en el test anterior, lo que hemos hecho ha sido pasar dos valores a través del input y el expected, de manera que podamos verificar nuestro método.
Uso de ficheros CSV con parametized test
También podemos hacer, que en lugar de cargar valores directamente en el método, se carguen a través de un fichero CSV.
Para ello lo único que necesitamos es crear un fichero CSV de dos columnas en donde la cabecera de las columnas llevarán los nombres de los parámetros de entrada. Aunque también podemos poner el parámetro numLinesToSkip = 1 y saltar la cabecera.
Vamos a ver el ejemplo:
input,expected test,TEST tEst,TEST Java,JAVA
@ParameterizedTest @CsvFileSource(resources = "/numbers.csv") void given_a_number_when_plus_then_should_generate_the_expected(int input, int expected) { int value= Test.plusNumber(input); assertEquals(expected, value); }
Uso de Enum con Parametized Test
Siguiendo con la automatización de los test podemos hacer uso de la anotación @EnumSource para asignar valores cuando tratamos con enumerados. Por ejemplo:
@ParameterizedTest @EnumSource(DayOfWeek.class) void given_week_value_are_bettwen_1_and_7(DayOfWeek week) { int day= week.getValue(); assertTrue(day>= 1 && day<= 7); }
DayOfWeek es un enum de java.time
Al ejecutar el test anterior se va a ejecutar una vez por cada valor del enum.
Utilizar métodos para verificar test con Parametized Test
Otra de las funcionalidades nuevas que han venido con JUnit 5 y los parametized Test es la posibilidad de pasar métodos para verificar nuestros test. Ya que los ejemplos que hemos visto hasta ahora únicamente nos permiten pasar valores simples, vamos a ver como haciendo uso de @MethodSource podemos invocar a un método para devolver argumentos.
Los valores que puede devolver el método que se invoca son los siguientes:
- Stream
- Iterable
- Iterator
- Array de argumentos
Vamos a ver un ejemplo. Imagina que tenemos una clase para obtener el día de la semana asociado a un número y queremos analizar que la relación es correcta:
@ParameterizedTest @MethodSource("numberToDay") void numberToDay(int day, String name) { assertEquals(name, DayOfWeek.of(day)); } private static Stream<Arguments> numberToDay() { return Stream.of( arguments(1, "Sunday"), arguments(2, "Monday"), arguments(3, "Tuesday") arguments(4, "Wednesday"), arguments(5, "Thursday"), arguments(6, "Friday"), arguments(7, "Saturday") ); }
En el fragmento de código anterior se validará que es correcto el número pasado por parámetro con el número.
Si no pasamos el nombre del método, por defecto se buscará un nombre que coincida, en este caso sería numberToDay.
Compartir Argumentos con @ArgumentSource
Con @MethodSource podemos también hacer referencia a otros métodos, para ello tenemos que realizar toda la llamada al método, incluida el package:
package com.refactorizando; import java.util.stream.Stream; public class UpperString{ private static Stream<String> letters() { return Stream.of("AAA", "BBB", "CCC"); } }
Para llamar completamente al método hay que realizar una combinación completa de nombre del paquete, método y clase.
El método areEqualsAndUpper se encarga de verificar que las letras son iguales y en mayúsculas.
@ParameterizedTest @MethodSource("com.refactorizando.UpperString#letters") void equalsLettersAndUpper(String string) { assertTrue(StringUtils.areEqualsAndUpper(string)); }
Aunque la opción anterior es válida, es más recomendable escribir una clase custom que implemente ArgumentsProvider:
public class EqualsLetterProvider implements ArgumentsProvider { @Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) { return Stream.of("AAA", "BBB", "CCC").map(Arguments::of); } }
Una vez realizado la clase anterior podemos asignarlo a la anotación @ArgumentsSource.
@ParameterizedTest @ArgumentsSource(EqualsLetterProvider.class) void equalsLettersAndUpper(String string) { assertTrue(StringUtils.areEqualsAndUpper(string)); }
Convertir argumentos con Parametized Test
Una de las funcionalidades de JUnit 5 es la de permitirnos transformar valores de un String a un enum a través de un conversor de tipos. Para ver más información y los tipos que se pueden convertir de manera ímplicita puedes echar un ojo a la conversión de tipos de JUnit 5.
Gracias a la conversión de tipos ímplicita podríamos hacer algo así:
@ParameterizedTest @CsvSource({"MONDAY", "TUESDAY", "WEDNESDAY"}) void given_week_value_are_bettwen_1_and_7(DayOfWeek week) { int day= week.getValue(); assertTrue(day>= 1 && day<= 7); }
Crear conversor de argumentos con Parametized Test
JUnit 5 solo nos da algunas conversiones especifícas por lo que para algunas otras necesitamos crear un conversor específico. Por lo que cada vez que usemos el conversor específico que hemos creado lo haremos con la anotación @ConvertWith.
Vamos a hacer un conversor para convertir de String a int, siempre y cuando sean números.
Para hacer un conversor custom podemos hacerlo con la clase ArgumentConverter o con TypedArgumentConverter, como va a ser nuestro caso, por hacer uso de un tipo.
class StringToIntConverter extends TypedArgumentConverter<String, Integer> { protected StringToIntConverter () { super(String.class, Integer.class); } @Override public Integer convert(String source) throws ArgumentConversionException { try { return Integer.parseInt(source); } catch (NumberFormatException e) { throw new ArgumentConversionException("Cannot convert string to int", e.getMessage()); } } }
Para testear nuestro conversor de tipos lo hacemos de la siguiente manera:
@ParameterizedTest @CsvSource({ "1, 1", "1, 1", "2, 2" }) void convertWithCustomHexConverter(int number, @ConvertWith(StringToIntConverter.class) int result) { assertEquals(number, result); }
Personalización de los nombres en la ejecución de los test con Parametized Test con JUnit 5
Cuando ejecutamos nuestros test con JUnit 5 aparecen con la invocación del índice y un string con todos los parámetros. Pero si queremos modificar esta representación lo podemos hacer añadiendo argumentos a ParametizedTest para especificar la manera en la que se «pintará» nuestros test:
@ParameterizedTest(name = "{index} => number={0}, day={1}") @MethodSource void numberToDay(int day, String name) { assertEquals(name, DateWeekUtils.getWeekDay(day)); } private static Stream<Arguments> numberToDay() { return Stream.of( arguments(1, "Sunday"), arguments(2, "Monday"), arguments(3, "Tuesday") arguments(4, "Wednesday"), arguments(5, "Thursday"), arguments(6, "Friday"), arguments(7, "Saturday") ); }
Con la parametrización que hemos hecho pasando el número (number) y el el día como argumentos obtendremos el resultado de los test de una manera más ordenada.
monthNames(int, String) ├─ 1 => number=1, month=Sunday ├─ 2 => number=2, month=Monday ├─ 3 => number=3, month=Tuesday ├─ 4 => number=4, month=Wednesday ├─ 5 => number=5, month=Thursday ├─ 6 => number=6, month=Friday └─ 7 => number=7, month=Saturday
Conclusión
En esta entrada sobre Uso de Parametized Test con JUnit 5 hemos visto diversos ejemplos sobre su uso y como nos puede ayudar a realizar test automatizados.
Esta nueva funcionalidad de JUnit 5 nos va a ser de gran ayudar para poder añadir una mayor verificación a nuestros test y de esta manera asegurar que nuestras aplicaciones funcionan de manera correcta.
Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com o también nos puedes contactar por nuestras redes sociales Facebook o twitter y te ayudaremos encantados!
Hola, en el caso de los datos csv es necesario agregar ‘value’:
@ParameterizedTest
@CsvSource(value={«2:4», «6:8», «10:12»}, delimiter = ‘:’)
…
¡Enhorabuena y gracias por el trabajo!
Muchas gracias por tu comentario Jose Antonio! :), gracias por la correción.