Tests paramétrés JUnit en Java
Exécutez le même test JUnit avec différentes entrées grâce à @ParameterizedTest et aux sources de valeurs.
Un test paramétré exécute la même méthode de test plusieurs fois, une fois pour chaque ensemble d'entrées fourni. Au lieu de copier-coller testReverseAbc, testReverseEmpty et testReverseSingle, vous écrivez la logique une seule fois et fournissez une source de données — une liste d'entrées et de résultats attendus. JUnit 5 (le moteur Jupiter) gère cela nativement avec @ParameterizedTest et une famille d'annotations de source. Le bénéfice : moins de lignes, une couverture plus dense, et chaque entrée rapportée comme un résultat distinct de réussite ou d'échec.
Ce chapitre suppose que vous savez déjà comment écrire et asserter un test simple ; sinon, commencez par l'introduction à JUnit et les assertions JUnit. Il couvre les cas où recourir à un test paramétré, comment choisir une source d'arguments (@ValueSource, @CsvSource, @MethodSource et autres), ainsi que l'erreur la plus courante — une valeur attendue incorrecte plutôt qu'un bug dans le code.
Des tests répétés à un seul test paramétré
Une méthode @Test ordinaire teste exactement un scénario. Lorsque vous souhaitez vérifier le même comportement sur une table d'entrées, l'approche naïve répète la méthode :
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 version paramétrée regroupe les trois en une seule méthode. Vous annotez avec @ParameterizedTest (et non @Test) et attachez une source qui fournit l'argument à chaque exécution :
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 invoque isPrime trois fois — candidate=2, puis 7, puis 13 — et rapporte trois résultats. Un échec sur une valeur ne masque pas les autres.
Choisir une source d'arguments
L'annotation @ParameterizedTest est inutile seule ; elle a besoin d'une source qui produit les arguments. JUnit Jupiter en propose plusieurs, chacune adaptée à une forme de données différente :
| Source | Fournit | Idéal pour |
|---|---|---|
@ValueSource | Un seul littéral par exécution (ints, strings, doubles, …) | Tests à un seul argument |
@CsvSource | Une ligne de valeurs séparées par des virgules par exécution | Quelques lignes inline avec plusieurs colonnes |
@CsvFileSource | Des lignes lues depuis un fichier .csv du classpath | Tables volumineuses ou maintenues en externe |
@MethodSource | Ce que renvoie une méthode de fabrique sous forme de Stream/Collection | Objets complexes, cas calculés |
@EnumSource | Les constantes d'un enum | Couverture exhaustive d'un enum |
@NullSource / @EmptySource | Valeurs null et vides | Couverture des cas limites de strings/collections |
La règle générale : @ValueSource pour une seule entrée simple, @CsvSource pour une petite table multi-colonnes, et @MethodSource dès que les données ne tiennent plus dans des littéraux d'annotation.
Plusieurs colonnes avec @CsvSource
Lorsque chaque cas possède une entrée et une sortie attendue, @CsvSource vous offre une petite table inline. Chaque string est une ligne ; les virgules la divisent en paramètres de méthode dans l'ordre :
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 convertit chaque jeton séparé par des virgules vers le type de paramètre déclaré, donc @CsvSource({"4, 16"}) peut atterrir dans (int n, int square). Utilisez des guillemets simples pour inclure des virgules ou des strings vides dans une cellule.
Cas calculés avec @MethodSource
Les valeurs d'annotation doivent être des constantes à la compilation ; dès que les arguments sont de vrais objets ou nécessitent un calcul, passez à @MethodSource. Il désigne une méthode statique qui renvoie un Stream<Arguments> (ou n'importe quelle Collection/tableau) :
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));
}
}L'attribut name optionnel personnalise l'affichage de chaque invocation dans le rapport de test, avec {0}, {1} remplaçant les arguments — très utile pour identifier d'un coup d'œil quelle ligne a échoué.
Exemple complet : un lanceur paramétré sans JUnit
Le lanceur de code n'a pas JUnit dans son classpath, ce programme modélise donc le mécanisme qu'un test paramétré incarne avec du code JDK pur : une seule vérification est définie une fois, puis appliquée sur une liste de cas — exactement ce que @ParameterizedTest fait derrière les annotations. Un cas est intentionnellement erroné pour vous montrer comment des lignes isolées passent ou échouent.
Ce qu'il faut retenir de l'exécution :
- Le bloc
reverseaffiche quatre lignesPASSet>> reverse: 4 passed, 0 failed— un seul corps (reverse) s'est exécuté sur quatre lignes, reflétant comment une seule méthode@ParameterizedTestest invoquée une fois par ligne@CsvSource. - Le bloc
isPrimeaffichePASSpour les entrées2,7,9et1, maisFAILpour l'entrée4, carisPrime(4)retournefalsealors que la ligne affirmaittrue— une attente incorrecte, pas un bug dans le code, ce qui est l'erreur la plus courante dans les tests paramétrés. - Cet échec unique est rapporté sur sa propre ligne et compté comme
>> isPrime: 4 passed, 1 failed; les autres lignes passent toujours, démontrant l'avantage clé par rapport à une boucle manuelle avec une seule assertion — chaque entrée est un cas indépendant, rapporté individuellement. - Le helper
runAllprend l'unité sous forme deFunctionet les cas sous forme deList, séparant la logique testée des données — exactement la séparation que@ParameterizedTestplus une source d'arguments vous procure. - Chaque ligne affiche
expectedà côté deactual, donc la ligne4 / expected=true / actual=falsevous indique précisément quelle valeur a divergé — la même valeur diagnostique que fournissent le message d'assertEqualsde JUnit et le templatename = "...".
Quand utiliser un test paramétré
Optez pour @ParameterizedTest quand un comportement doit être vérifié sur une table d'entrées — valeurs limites, classes d'équivalence, ou une liste de régression d'entrées qui ont déjà causé des bugs. Continuez à utiliser un @Test ordinaire quand un scénario nécessite une configuration unique ou des assertions distinctes ; regrouper des cas sans rapport dans une seule méthode paramétrée ne fait que rendre le rapport plus difficile à lire. Pour la configuration partagée entre les deux styles, consultez le chapitre sur le cycle de vie des tests, et pour le vocabulaire complet des assertions utilisées dans chaque exécution, le chapitre sur les assertions.