Les tests paramétrés permettent aux développeurs de tester efficacement leur code avec une gamme de valeurs d’entrée. Dans le domaine des tests JUnit, les utilisateurs expérimentés se sont longtemps débattus avec la complexité de la mise en œuvre de ces tests. Mais avec la sortie de JUnit 5.7, une nouvelle ère de test de paramétrage entre en vigueur, offrant aux développeurs un support de premier ordre et des capacités améliorées. Explorons les possibilities passionnantes que JUnit 5.7 apporte à la table pour les tests paramétrés !
Échantillons de paramétrisation à partir des docs JUnit 5.7
Voyons quelques exemples à partir des docs :
@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"))
);
}
L’annotation @ParameterizedTest
doit être accompagnée d’une des plusieurs annotations source fournies décrivant d’où prendre les paramètres. La source des paramètres est souvent appelée le « fournisseur de données ».
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:
- L’
@ValueSource
est limité à fournir une seule valeur de paramètre uniquement. En d’autres termes, la méthode de test ne peut pas avoir plus d’un argument, et les types que l’on peut utiliser sont également restreints. - Passer plusieurs arguments est partiellement abordé par
@CsvSource
, en analysant chaque chaîne en un enregistrement qui est ensuite passé comme arguments champ par champ. Cela peut devenir difficile à lire avec des chaînes longues et/ou un grand nombre d’arguments. Les types que l’on peut utiliser sont également restreints — nous y reviendrons plus tard. - Toutes les sources qui déclarent les valeurs réelles dans les annotations sont limitées à des valeurs constantes à la compilation (limitation des annotations Java, pas de JUnit).
@MethodSource
et@ArgumentsSource
fournissent un flux/collection de n-uplets (non typés) qui sont ensuite passés comme arguments de méthode. Divers types réels sont pris en charge pour représenter la séquence de n-uplets, mais aucun d’eux ne garantit qu’ils correspondront à la liste des arguments de la méthode. Ce type de source nécessite des méthodes ou des classes supplémentaires, mais il n’impose aucune restriction sur l’endroit et la manière d’obtenir les données de test.
Comme vous pouvez le voir, les types de sources disponibles vont des plus simples (faciles à utiliser, mais limités en fonctionnalité) aux plus flexibles qui nécessitent plus de code pour fonctionner.
- Remarque en passant — Ceci est généralement un signe d’un bon design : un peu de code est nécessaire pour les fonctionnalités essentielles, et ajouter de la complexité supplémentaire est justifié lorsque cela permet de répondre à un cas d’utilisation plus exigeant.
Ce qui ne semble pas s’inscrire dans ce continuum hypothétique du simple au flexible, c’est @EnumSource
. Jetons un œil à cet exemple non trivial de quatre jeux de paramètres avec 2 valeurs chacun.
- Remarque — Alors que
@EnumSource
passe la valeur de l’enum comme un seul paramètre de méthode de test, conceptuellement, le test est paramétré par les champs de l’enum, ce qui ne pose aucune restriction sur le nombre de paramètres.
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());
}
Pensez-y : la liste de valeurs hardcoded restreint sévèrement sa flexibilité (pas de données externes ou générées), tandis que la quantité de code supplémentaire nécessaire pour déclarer l’enum
rend cette alternative plutôt verbale par rapport, disons, à @CsvSource
.
mais c’est juste une première impression. Nous verrons comment cela peut devenir élégant en exploitant la véritable puissance des enums Java.
- Remarque : cet article ne traite pas de la vérification des enums qui font partie de votre code de production. Ceux-ci, bien sûr, devaient être déclarés peu importe comment vous choisissez de les vérifier. Au lieu de cela, il se concentre sur quand et comment exprimer vos données de test sous forme d’enums.
Quando l’utiliser
Il y a des situations où les enums sont plus performantes que les alternatives :
Plusieurs paramètres par test
Quando avez besoin d’un seul paramètre, vous ne souhaitez probablement pas compliquer les choses au-delà de @ValueSource
. Mais dès que vous avez besoin de plusieurs paramètres – disons, des entrées et des résultats attendus – vous devez recourir à @CsvSource,
@MethodSource/@ArgumentsSource
ou @EnumSource
.
De manière à ce que l’enum
vous permette de « s’incruster » n’importe quel nombre de champs de données.
Alors, lorsque vous avez besoin d’ajouter plus de paramètres de méthode de test à l’avenir, vous ajoutez simplement plus de champs dans vos enums existants, laissant les signatures de méthode de test intactes. Cela devient précieux lorsque vous réutilisez votre fournisseur de données dans plusieurs tests.
Pour d’autres sources, il faut utiliser ArgumentsAccessor
s ou ArgumentsAggregator
s pour la flexibilité que les enums ont par défaut.
Sécurité de type
Pour les développeurs Java, cela devrait être un grand avantage.
Les paramètres lus à partir de CSV (fichiers ou littéraux), @MethodSource
ou @ArgumentsSource
, ne fournissent aucune garantie à la compilation que le nombre de paramètres et leurs types vont correspondre à la signature.
Évidemment, JUnit va se plaindre à l’exécution mais oubliez toute assistance de code de votre IDE.
Comme avant, cela s’ajoute lorsque vous réutilisez les mêmes paramètres pour plusieurs tests. Utiliser une approche sécurisée par type serait un énorme avantage lors de l’extension de l’ensemble de paramètres à l’avenir.
Types personnalisés
C’est principalement un avantage par rapport aux sources basées sur du texte, telles que celles qui lisent des données à partir de CSV — les valeurs encodées dans le texte doivent être converties en types Java.
Si vous avez une classe personnalisée à instancier à partir de l’enregistrement CSV, vous pouvez le faire en utilisant ArgumentsAggregator
. Cependant, votre déclaration de données n’est toujours pas sécurisée par type — toute incohérence entre la signature de la méthode et les données déclarées apparaîtra à l’exécution lors de l' »agrégation » des arguments. Sans parler que déclarer la classe agrégatrice ajoute plus de code de support nécessaire pour que votre paramétrage fonctionne. Et nous avons toujours préféré @CsvSource
à @EnumSource
pour éviter le code supplémentaire.
Documentable
Contrairement aux autres méthodes, la source enum拥有Java符号,既用于参数集(枚举实例),也用于它们包含的所有参数(枚举字段)。 Ils fournissent un endroit simple et naturel pour attacher de la documentation sous forme de JavaDoc.
Ce n’est pas que la documentation ne peut pas être placée ailleurs, mais par définition, elle sera plus éloignée de ce qu’elle documente et donc plus difficile à trouver, et plus susceptible de devenir obsolète.
Mais il y a plus !
Maintenant : Enums. Sont. Des. Classes.
Il semble que de nombreux développeurs juniors ne réalisent pas encore à quel point les enums Java sont truly puissantes.
Dans d’autres langages de programmation, elles ne sont vraiment que des constantes améliorées. Mais en Java, elles sont des petites implémentations du motif de conception Flyweight avec (beaucoup de) avantages des classes complets.
Pourquoi est-ce une bonne chose ?
Comportement lié aux fixtures de test
Comme toute autre classe, les enums peuvent avoir des méthodes ajoutées.
Cela devient pratique si les paramètres de test enum sont réutilisés entre les tests – mêmes données, juste testées un peu différemment. Pour travailler efficacement avec les paramètres sans recourir à une copie et collage significative, du code d’aide doit être partagé entre ces tests.
Ce n’est pas quelque chose qu’une classe d’aide et quelques méthodes statiques ne pourraient pas « résoudre ».
- Note à part : Notez que tel design souffre d’un envy de fonctionnalité. Les méthodes de test — ou pire, les méthodes de classe helper — devraient extraire les données des objets enum pour effectuer des actions sur ces données.
Bien que ceci soit (seule) la voie dans la programmation procédurale, dans le monde orienté objet, nous pouvons faire mieux.
Déclarer les méthodes « helper » directement dans la déclaration de l’enum, nous déplacerions le code là où se trouve les données. Ou, pour utiliser le jargon OOP, les méthodes helper deviendraient le « comportement » des fixtures de test implémentées comme enums. Cela rendrait non seulement le code plus idiomatic (en appelant des méthodes sensées sur des instances plutôt que des méthodes statiques transmettant des données), mais cela rendrait également plus facile la réutilisation de paramètres enum à travers les cas de test.
Héritage
Les enums peuvent implémenter des interfaces avec des méthodes (par défaut). Lorsqu’elles sont utilisées judicieusement, cela peut être utilisé pour partager le comportement entre plusieurs fournisseurs de données — plusieurs enums.
Un exemple qui vient facilement à l’esprit est l’utilisation de enums distincts pour les tests positifs et négatifs. Si elles représentent un type similaire de fixture de test, il y a de fortes chances qu’elles aient un comportement à partager.
Le discours est bon marché
Illustrons cela sur une suite de tests d’un convertisseur hypothétique de fichiers de code source, pas tout à fait unlike celui effectuant la conversion de Python 2 vers Python 3.
Pour avoir une véritable confiance dans ce que fait un outil si complet, on se retrouverait avec un ensemble étendu de fichiers d’entrée manifestant divers aspects de la langue, et des fichiers correspondants pour comparer le résultat de conversion. Sauf cela, il est nécessaire de vérifier quels avertissements/erreurs sont servis à l’utilisateur pour les entrées problématiques.
Cela correspond naturellement aux tests paramétrés en raison du grand nombre d’échantillons à vérifier, mais cela ne correspond pas vraiment à l’une des sources de paramètres simples de JUnit, car les données sont quelque peu complexes.
Voir ci-dessous:
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"));
// Beaucoup, beaucoup d'autres ...
@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);
}
}
L’utilisation des enums ne nous restreint pas dans la complexité des données. Comme vous pouvez le voir, nous pouvons définir plusieurs constructeurs pratiques dans les enums, donc déclarer de nouveaux ensembles de paramètres est agréable et propre. Cela empêche l’utilisation de longues listes d’arguments qui finissent souvent remplies de nombreux valeurs « vides » (null, chaînes vides ou collections) qui laissent quelqu’un se demander ce que représente l’argument n°7 — vous savez, l’un des nulls.
Noticez comment les enums permettent l’utilisation de types complexes (Set
, RuntimeException
) sans restrictions ou conversions magiques. Transmettre telles données est également completely type-safe.
Je sais ce que vous pensez. C’est très verbeux. Eh bien, jusqu’à un certain point. Réalistement, vous allez avoir beaucoup plus d’échantillons de données à vérifier, donc la quantité de code de remplacement sera moins significative en comparaison.
En outre, voyez comment les tests liés peuvent être écrits en utilisant les mêmes enums, et leurs méthodes d’aide :
@ParameterizedTest
@EnumSource
// Mise à niveau des fichiers déjà mis à jour passe toujours, ne fait aucune modification, ne délivre aucun avertissement.
void upgradeFromV3toV3AlwaysPasses(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
}
@ParameterizedTest
@EnumSource
// La réduction de fichiers créés par la procédure de mise à niveau est censée toujours passer sans avertissements.
void downgrade(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV2File());
}
Un peu plus de discussions après tout
Conceptuellement, @EnumSource
vous encourage à créer une description complexe et lisible par machine de chaque scénario de test, brouillant la frontière entre les fournisseurs de données et les fixtures de test.
Une autre grande chose à propos de l’utilisation de chaque ensemble de données exprimé comme un symbole Java (élément d’énumération) est qu’ils peuvent être utilisés individuellement ; complètement en dehors des fournisseurs de données/test paramétrés. Puisque’ils ont un nom raisonnable et qu’ils sont autonomes (en termes de données et de comportement), ils contribuent à des tests nets et lisibles.
@Test
void warnWhenNoEventsReported() throws Exception {
FixtureXmls.Invalid events = FixtureXmls.Invalid.NO_EVENTS_REPORTED;
// read() est une méthode assistant partagée par tous les FixtureXmls
try (InputStream is = events.read()) {
EventList el = consume(is);
assertEquals(Set.of(...), el.getWarnings());
}
}
Jetzt, @EnumSource
ne sera pas l’un de vos sources d’arguments les plus fréquemment utilisées, et c’est une bonne chose, car l’utiliser en excès ne ferait pas de bien. Mais dans les bonnes circonstances, il est utile de savoir comment utiliser tout ce qu’ils ont à offrir.
Source:
https://dzone.com/articles/junit5-parameterized-tests-with-enumsource