Pruebas parametrizadas de Java JUnit
Ejecuta el mismo test JUnit con distintas entradas usando @ParameterizedTest y fuentes de valores.
Una prueba parametrizada ejecuta el mismo método de prueba muchas veces, una por cada conjunto de entradas que le proporcionas. En lugar de copiar y pegar testReverseAbc, testReverseEmpty y testReverseSingle, escribes la lógica una sola vez y suministras una fuente de datos — una lista de entradas y resultados esperados. JUnit 5 (el motor Jupiter) convierte esto en funcionalidad de primera clase con @ParameterizedTest y una familia de anotaciones de fuentes. El beneficio es menos líneas, mayor cobertura y cada entrada reportada como su propio resultado de éxito o fallo.
Este capítulo asume que ya sabes cómo se escribe y valida una prueba básica; si no es así, empieza con la introducción a JUnit y las aserciones de JUnit. Cubre cuándo usar una prueba parametrizada, cómo elegir una fuente de argumentos (@ValueSource, @CsvSource, @MethodSource y otras), y el error más común — un valor esperado incorrecto en lugar de un bug en el código.
De pruebas repetidas a una sola prueba parametrizada
Un método @Test simple prueba exactamente un escenario. Cuando quieres verificar el mismo comportamiento con una tabla de entradas, el enfoque ingenuo es repetir el método:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrimesTest {
@Test void two_isPrime() { assertTrue(Primes.isPrime(2)); }
@Test void seven_isPrime() { assertTrue(Primes.isPrime(7)); }
@Test void thirteen_isPrime() { assertTrue(Primes.isPrime(13)); }
}La versión parametrizada colapsa los tres en un solo método. Se anota con @ParameterizedTest (no @Test) y se adjunta una fuente que suministra el argumento para cada ejecución:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrimesTest {
@ParameterizedTest
@ValueSource(ints = {2, 7, 13})
void isPrime(int candidate) {
assertTrue(Primes.isPrime(candidate));
}
}JUnit invoca isPrime tres veces — candidate=2, luego 7, luego 13 — y reporta tres resultados. Un valor que falla no oculta a los demás.
Elegir una fuente de argumentos
La anotación @ParameterizedTest es inútil por sí sola; necesita una fuente que produzca los argumentos. JUnit Jupiter incluye varias, cada una adecuada a una forma diferente de datos:
| Fuente | Proporciona | Más indicada para |
|---|---|---|
@ValueSource | Un literal por ejecución (ints, strings, doubles, …) | Pruebas con un solo argumento |
@CsvSource | Una fila de valores separados por comas por ejecución | Pocas filas en línea con varias columnas |
@CsvFileSource | Filas leídas de un archivo .csv en el classpath | Tablas grandes o mantenidas externamente |
@MethodSource | Lo que devuelve un método de fábrica como Stream/Collection | Objetos complejos, casos calculados |
@EnumSource | Las constantes de un enum | Cubrir exhaustivamente un enum |
@NullSource / @EmptySource | Valores null y vacíos | Cobertura de casos límite con strings/colecciones |
La regla general: @ValueSource para una entrada simple, @CsvSource para una pequeña tabla con varias columnas, y @MethodSource cuando los datos ya no caben en los literales de la anotación.
Varias columnas con @CsvSource
Cuando cada caso tiene una entrada y una salida esperada, @CsvSource te ofrece una pequeña tabla en línea. Cada string es una fila; las comas la dividen en parámetros del método en orden:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class StringsTest {
@ParameterizedTest
@CsvSource({
"abc, cba",
"racecar, racecar",
"'', ''" // single quotes denote an empty string
})
void reverse(String input, String expected) {
assertEquals(expected, Strings.reverse(input));
}
}JUnit convierte cada token separado por coma al tipo de parámetro declarado, por lo que @CsvSource({"4, 16"}) puede recaer en (int n, int square). Usa comillas simples para incluir comas o strings vacíos dentro de una celda.
Casos calculados con @MethodSource
Los valores de anotación deben ser constantes en tiempo de compilación, por lo que cuando los argumentos son objetos reales o necesitan cálculo, cambia a @MethodSource. Nombra un método estático que devuelve un Stream<Arguments> (o cualquier Collection/array):
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TaxTest {
static Stream<Arguments> brackets() {
return Stream.of(
Arguments.of(0, 0.0),
Arguments.of(10_000, 1_000.0),
Arguments.of(50_000, 7_500.0)
);
}
@ParameterizedTest(name = "income {0} -> tax {1}")
@MethodSource("brackets")
void computesTax(int income, double expectedTax) {
assertEquals(expectedTax, Tax.of(income));
}
}El atributo opcional name personaliza cómo aparece cada invocación en el informe de pruebas, con {0}, {1} representando los argumentos — invaluable cuando hay que identificar a simple vista una fila con fallo.
Un ejemplo completo: un ejecutor parametrizado sin JUnit
El ejecutor de código no tiene JUnit en su classpath, por lo que este programa modela el mecanismo que encarna una prueba parametrizada con código JDK puro: una única verificación se define una vez y se ejecuta sobre una lista de casos — exactamente lo que hace @ParameterizedTest detrás de las anotaciones. Un caso es incorrecto a propósito para que puedas ver cómo las filas pasan o fallan de forma aislada.
Lo que hay que extraer de la ejecución:
- El bloque
reverseimprime cuatro líneasPASSy>> reverse: 4 passed, 0 failed— un cuerpo (reverse) se ejecutó contra cuatro filas, reflejando cómo un único método@ParameterizedTestse invoca una vez por fila de@CsvSource. - El bloque
isPrimeimprimePASSpara las entradas2,7,9y1, peroFAILpara la entrada4, porqueisPrime(4)devuelvefalsemientras que la fila declarabatrue— una expectativa incorrecta, no un bug en el código, que es el error más común en las pruebas parametrizadas. - Ese único fallo se reporta en su propia línea y se cuenta como
>> isPrime: 4 passed, 1 failed; las otras filas siguen pasando, demostrando la ventaja clave frente a un bucle manual con una sola aserción — cada entrada es un caso independiente reportado individualmente. - El helper
runAlltoma la unidad comoFunctiony los casos comoList, separando la lógica bajo prueba de los datos — exactamente la separación que te da@ParameterizedTestjunto con una fuente de argumentos. - Cada línea muestra
expectedjunto aactual, de modo que la fila4 / expected=true / actual=falsete dice exactamente qué valor difirió — el mismo valor diagnóstico que proporciona el mensaje deassertEqualsde JUnit y la plantillaname = "...".
Cuándo usar una prueba parametrizada
Recurre a @ParameterizedTest cuando un comportamiento debe cumplirse con una tabla de entradas — valores límite, clases de equivalencia o una lista de regresión de entradas que en algún momento fallaron. Sigue usando un @Test simple cuando un escenario necesita una configuración única o aserciones distintas; meter casos no relacionados en un único método parametrizado solo dificulta la lectura del informe. Para la configuración compartida en cualquier estilo, consulta el capítulo del ciclo de vida de las pruebas, y para el vocabulario completo de aserciones usado dentro de cada ejecución, el capítulo de aserciones.