Pruebas parametrizadas permiten a los desarrolladores probar su código de manera eficiente con una gama de valores de entrada. En el ámbito de las pruebas JUnit, los usuarios avezados han lidiado durante mucho tiempo con la complejidad de implementar estas pruebas. Pero con el lanzamiento de JUnit 5.7, una nueva era de prueba de parametrización comienza, ofreciendo a los desarrolladores soporte de primera categoría y capacidades mejoradas. Vamos a sumergirnos en las emocionantes posibilidades que JUnit 5.7 trae a la mesa para las pruebas parametrizadas!
Muestras de Parametrización Desde la Documentación de JUnit 5.7
Veamos algunos ejemplos desde la documentación:
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
La anotación @ParameterizedTest
debe ir acompañada de una de varias anotaciones de origen proporcionadas que describen desde dónde tomar los parámetros. La fuente de los parámetros a menudo se conoce como el “proveedor de datos”.
I will not dive into their detailed description here: the JUnit user guide does it better than I could, but allow me to share several observations:
- El
@ValueSource
se limita a proporcionar solo un valor de parámetro. En otras palabras, el método de prueba no puede tener más de un argumento, y los tipos que se pueden usar están restringidos también. - La passage de múltiples argumentos se aborda en cierta medida con
@CsvSource
, analizando cada cadena en un registro que luego se pasa como argumentos campo por campo. Esto puede volverse difícil de leer con cadenas largas y/o una gran cantidad de argumentos. Los tipos que se pueden usar también están restringidos, más sobre esto más adelante. - Todas las fuentes que declaran los valores reales en anotaciones están restringidas a valores que son constantes en tiempo de compilación (anotaciones de Java, no JUnit).
@MethodSource
y@ArgumentsSource
proporcionan un flujo/colección de n-tuplas (sin tipo) que luego se pasan como argumentos de método. Se admiten varios tipos reales para representar la secuencia de n-tuplas, pero ninguno de ellos garantiza que se ajustarán a la lista de argumentos del método. Este tipo de fuente requiere métodos o clases adicionales, pero no impone restricciones sobre dónde y cómo obtener los datos de prueba.
Como puede ver, los tipos de fuente disponibles van desde los simples (fáciles de usar, pero limitados en funcionalidad) hasta los más flexibles que requieren más código para funcionar.
- Nota al margen — Esto es generalmente una señal de un buen diseño: se necesita un poco de código para la funcionalidad esencial, y agregar complejidad adicional está justificado cuando se usa para habilitar un caso de uso más exigente.
Lo que no parece ajustarse a este hipotético continuo de simple a flexible, es @EnumSource
. Mire este ejemplo no trivial de cuatro conjuntos de parámetros con 2 valores cada uno.
- Nota — Mientras que
@EnumSource
pasa el valor de la enumeración como un único parámetro del método de prueba, conceptualmente, la prueba está parametrizada por los campos de la enumeración, lo que no impone restricciones en el número de parámetros.
enum Direction {
UP(0, '^'),
RIGHT(90, '>'),
DOWN(180, 'v'),
LEFT(270, '<');
private final int degrees;
private final char ch;
Direction(int degrees, char ch) {
this.degrees = degrees;
this.ch = ch;
}
}
@ParameterizedTest
@EnumSource
void direction(Direction dir) {
assertEquals(0, dir.degrees % 90);
assertFalse(Character.isWhitespace(dir.ch));
int orientation = player.getOrientation();
player.turn(dir);
assertEquals((orientation + dir.degrees) % 360, player.getOrientation());
}
Tan solo piensa en esto: la lista de valores hardcoded restringe severamente su flexibilidad (sin datos externos o generados), mientras que la cantidad de código adicional necesaria para declarar el enum
convierte esta opción en bastante verbosa en comparación con, por ejemplo, @CsvSource
.
Pero esa es solo una primera impresión. Veremos cómo puede volverse elegante esto cuando se aprovecha la verdadera potencia de los enums de Java.
- Nota al margen: Este artículo no aborda la verificación de enums que son parte de su código de producción. Esos, por supuesto, debían ser declarados sin importar cómo elija verificarlos. En cambio, se centra en cuándo y cómo expresar sus datos de prueba en forma de enums.
Cuándo Usarlo
Hay situaciones en las que los enums funcionan mejor que las alternativas:
Parámetros Múltiples por Prueba
Cuando solo necesitas un parámetro, probablemente no querrás complicar las cosas más allá de @ValueSource
. Pero en cuanto necesitas múltiples parámetros, digamos, entradas y resultados esperados, tienes que recurrir a @CsvSource,
@MethodSource/@ArgumentsSource
o @EnumSource
.
De alguna manera, el enum
te permite “introducir” cualquier número de campos de datos.
Así que cuando necesites agregar más parámetros a tu método de prueba en el futuro, simplemente agregas más campos en tus enums existentes, dejando las firmas de los métodos de prueba sin tocar. Esto se vuelve invaluable cuando reutilizas tu proveedor de datos en múltiples pruebas.
Para otras fuentes, uno debe emplear ArgumentsAccessor
s o ArgumentsAggregator
s por la flexibilidad que tienen los enums de forma nativa.
Seguridad de Tipos
Para los desarrolladores de Java, esto debería ser un aspectomuy importante.
Parámetros leídos desde CSV (archivos o literales), @MethodSource
o @ArgumentsSource
, no proporcionan ninguna garantía en tiempo de compilación de que el conteo de los parámetros, y sus tipos, van a coincidir con la firma.
Obviamente, JUnit se quejará en tiempo de ejecución pero olvídate de cualquier asistencia de código de tu IDE.
Lo mismo que antes, esto se acumula cuando reutiliza los mismos parámetros para múltiples pruebas. Usar un enfoque seguro de tipos sería una gran ventaja al ampliar el conjunto de parámetros en el futuro.
Tipos Personalizados
Esto es principalmente una ventaja sobre fuentes basadas en texto, como las que leen datos de CSV — los valores codificados en el texto necesitan ser convertidos a tipos Java.
Si tienes una clase personalizada para instanciar desde el registro de CSV, puedes hacerlo usando ArgumentsAggregator
. Sin embargo, tu declaración de datos aún no es segura de tipos — cualquier discrepancia entre la firma del método y los datos declarados aparecerá en tiempo de ejecución al “agregar” argumentos. No olvidemos que declarar la clase del聚合ador añade más código de apoyo necesario para que funcione tu parametrización. Y siempre hemos favorecido @CsvSource
sobre @EnumSource
para evitar el código adicional.
Documentable
A diferencia de otros métodos, lafuente de enum tiene símbolos de Java para ambos conjuntos de parámetros (instancias de enum) y todos los parámetros que contienen (campos de enum). Proporcionan un lugarfrancamente claro donde adjuntar documentación en su forma más natural — el JavaDoc.
No es que la documentación no pueda colocarse en otro lugar, pero, por definición, se colocará más lejos de lo que documenta y, por lo tanto, será más difícil de encontrar y más fácil que se desactualice.
Pero hay más!
Ahora: Enums. Son. Clases.
Se tiene la sensación deque muchos desarrolladores junior aún no se han dado cuenta de lo poderosos que realmente son los enums de Java.
En otros lenguajes de programación, realmente son solo constantes glorificadas. Pero en Java, son convenientes pequeñas implementaciones de un patrón de diseño Flyweight con (mucho de) las ventajas de clases completas.
¿Por qué eso es una buena cosa?
Comportamiento relacionado con lasinstalaciones de prueba
Como con cualquier otra clase, los enums pueden tener métodos añadidos.
Esto se convierte en útil si los parámetros de prueba de enum se reutilizan entre pruebas — mismos datos, solo probados un poco differently.
No es algo que una clase auxiliar y unos pocos métodos estáticos no podrían “resolver”.
- Nota al margen: Cabe notar que dicho diseño padece de Envidia de Características. Los métodos de prueba, o peor aún, métodos de clases auxiliares, tendrían que extraer los datos de los objetos枚num para realizar acciones en esos datos.
Mientras que esta es la (única) manera en la programación procedural, en el mundo orientado a objetos, podemos hacer mejor.
Declarando los métodos “auxiliares” directamente en la declaración枚num, moveríamos el código donde están los datos. O, para ponerlo en jerga de POO, los métodos auxiliares se convertirían en el “comportamiento” de las instancias de prueba implementadas como枚num. Esto no solo haría que el código sea más idiomático (llamando métodos sensatos en instancias en lugar de métodos estáticos pasando datos), sino que también facilitaría la reutilización de parámetros枚num en varios casos de prueba.
Herencia
Los枚num pueden implementar interfaces con métodos (por defecto). Cuando se usa con sentido común, esto se puede aprovechar para compartir comportamientos entre varios proveedores de datos — varios枚num.
Un ejemplo que fácilmente viene a la mente es tener枚num separados para pruebas positivas y negativas. Si representan un tipo similar de fixture de prueba, es probable que tengan algún comportamiento para compartir.
La Palabra Es Barata
Ilustremos esto con una suite de pruebas de un hipotético conversor de archivos de código fuente, no muy diferente del que realiza la conversión de Python 2 a 3.
Para tener verdadera confianza en lo que hace una herramienta tan completa, uno terminaría con un conjunto extenso de archivos de entrada que manifiestan varios aspectos del lenguaje, y archivos de coincidencia para comparar el resultado de conversión. Excepto eso, es necesario verificar qué advertencias/errores se presentan al usuario para las entradas problemáticas.
Esto es una combinación natural para pruebas parametrizadas debido a la gran cantidad de muestras para verificar, pero no coincide exactamente con ninguna de las fuentes de parámetros simples de JUnit, ya que los datos son algo complejos.
Ver a continuación:
enum Conversion {
CLEAN("imports-correct.2.py", "imports-correct.3.py", Set.of()),
WARNINGS("problematic.2.py", "problematic.3.py", Set.of(
"Using module 'xyz' that is deprecated"
)),
SYNTAX_ERROR("syntax-error.py", new RuntimeException("Syntax error on line 17"));
// Muchos, muchos otros ...
@Nonnull
final String inFile;
@CheckForNull
final String expectedOutput;
@CheckForNull
final Exception expectedException;
@Nonnull
final Set expectedWarnings;
Conversion(@Nonnull String inFile, @Nonnull String expectedOutput, @NotNull Set expectedWarnings) {
this(inFile, expectedOutput, null, expectedWarnings);
}
Conversion(@Nonnull String inFile, @Nonnull Exception expectedException) {
this(inFile, null, expectedException, Set.of());
}
Conversion(@Nonnull String inFile, String expectedOutput, Exception expectedException, @Nonnull Set expectedWarnings) {
this.inFile = inFile;
this.expectedOutput = expectedOutput;
this.expectedException = expectedException;
this.expectedWarnings = expectedWarnings;
}
public File getV2File() { ... }
public File getV3File() { ... }
}
@ParameterizedTest
@EnumSource
void upgrade(Conversion con) {
try {
File actual = convert(con.getV2File());
if (con.expectedException != null) {
fail("No exception thrown when one was expected", con.expectedException);
}
assertEquals(con.expectedWarnings, getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
} catch (Exception ex) {
assertTypeAndMessageEquals(con.expectedException, ex);
}
}
El uso de enums no nos restringe en cuán complejos pueden ser los datos. Como pueden ver, podemos definir varios constructores convenientes en los enums, por lo que declarar nuevos conjuntos de parámetros eslimpio y ordenado. Esto evita el uso de largas listas de argumentos que a menudo terminan llenas de muchos valores “vacíos” (nulos, cadenas vacías o colecciones) que dejan a uno preguntándose qué representa el argumento número 7, ya sabes, uno de los nulos.
Noten cómo los enums permiten el uso de tipos complejos (Set
, RuntimeException
) sin restricciones ni conversiones mágicas. Pasar tales datos también es completamente seguro tipográficamente.
Ahora, sé lo que están pensando. Esto es awfully prolijo. Bueno, hasta cierto punto. Realísticamente, vas a tener muchos más datos de muestra para verificar, por lo que la cantidad de código de plantilla será menos significativa en comparación.
También, vean cómo las pruebas relacionadas pueden escribirse utilizando los mismos enums, y sus métodos auxiliares:
@ParameterizedTest
@EnumSource
// Actualizar archivos ya actualizados siempre pasa, no realiza cambios, no emite advertencias.
void upgradeFromV3toV3AlwaysPasses(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
}
@ParameterizedTest
@EnumSource
// Descender archivos creados por el procedimiento de actualización se espera que siempre pase sin advertencias.
void downgrade(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV2File());
}
Algo más de charla después de todo
Conceptualmente, @EnumSource
te anima a crear una descripción compleja y legible por máquina de cada escenario de prueba, difuminando la línea entre proveedores de datos y fijas de prueba.
Otra gran ventaja de tener cada conjunto de datos expresado como un símbolo de Java (elemento de enumeración) es que pueden ser utilizados individualmente; completamente fuera de los proveedores de datos/pruebas parametrizadas. Dado que tienen un nombre razonable y son autocontenidos (en términos de datos y comportamiento), contribuyen a pruebas agradables y legibles.
@Test
void warnWhenNoEventsReported() throws Exception {
FixtureXmls.Invalid events = FixtureXmls.Invalid.NO_EVENTS_REPORTED;
// read() es un método auxiliar que es compartido por todos los FixtureXmls
try (InputStream is = events.read()) {
EventList el = consume(is);
assertEquals(Set.of(...), el.getWarnings());
}
}
Ahora bien, @EnumSource
no va a ser una de tus fuentes de argumentos más utilizadas, y eso es una buena cosa, ya que su uso excesivo no sería beneficioso. Pero en las circunstancias correctas, resulta útil saber cómo aprovechar todo lo que tienen para ofrecer.
Source:
https://dzone.com/articles/junit5-parameterized-tests-with-enumsource