Dans JUnit 4, il était facile de tester des invariants à travers un tas de classes en utilisant @Parameterized
annotation. L'essentiel est qu'une collection de tests soit exécutée sur une seule liste d'arguments.
Comment reproduire cela dans JUnit 5, sans utiliser JUnit-vintage?
@ParameterizedTest
ne s'applique pas à une classe de test. @TestTemplate
semblait approprié, mais la cible de cette annotation est également une méthode.
Un exemple d'un tel test JUnit 4 est:
@RunWith( Parameterized.class )
public class FooInvariantsTest{
@Parameterized.Parameters
public static Collection<Object[]> data(){
return new Arrays.asList(
new Object[]{ new CsvFoo() ),
new Object[]{ new SqlFoo() ),
new Object[]{ new XmlFoo() ),
);
}
private Foo fooUnderTest;
public FooInvariantsTest( Foo fooToTest ){
fooUnderTest = fooToTest;
}
@Test
public void testInvariant1(){
...
}
@Test
public void testInvariant2(){
...
}
}
La fonction de test paramétré dans JUnit 5 ne fournit pas exactement les mêmes fonctionnalités que celles fournies par JUnit 4.
De nouvelles fonctionnalités avec plus de flexibilité ont été introduites ... mais il a également perdu la fonctionnalité JUnit4 où la classe de test paramétrée utilise les fixtures/assertions paramétrées au niveau de la classe qui est pour toutes les méthodes de test de la classe.
Il est donc nécessaire de définir @ParameterizedTest
Pour chaque méthode de test en spécifiant "l'entrée".
Au-delà de ce manque, je présenterai les principales différences entre les 2 versions et comment utiliser les tests paramétrés dans JUnit 5.
TL; DR
Pour écrire un test paramétré qui spécifie une valeur au cas par cas à tester comme votre dans votre question, org.junit.jupiter.params.provider.MethodSource
devrait faire le travail.
@MethodSource
Vous permet de vous référer à une ou plusieurs méthodes de la classe de test. Chaque méthode doit renvoyer unStream
,Iterable
,Iterator
ou un tableau d'arguments. De plus, chaque méthode ne doit accepter aucun argument. Par défaut, ces méthodes doivent être statiques sauf si la classe de test est annotée avec@TestInstance(Lifecycle.PER_CLASS)
.Si vous n'avez besoin que d'un seul paramètre, vous pouvez renvoyer directement des instances du type de paramètre, comme illustré dans l'exemple suivant.
Comme JUnit 4, @MethodSource
Repose sur une méthode d'usine et peut également être utilisé pour des méthodes de test qui spécifient plusieurs arguments.
Dans JUnit 5, c'est la façon d'écrire les tests paramétrés les plus proches de JUnit 4.
JUnité 4:
@Parameters
public static Collection<Object[]> data() {
Unité 5:
private static Stream<Arguments> data() {
Améliorations principales:
Collection<Object[]>
Est devenu Stream<Arguments>
Qui offre plus de flexibilité.
la façon de lier la méthode d'usine à la méthode d'essai diffère un peu.
Il est désormais plus court et moins sujet aux erreurs: plus besoin de créer un constructeur et déclare un champ pour définir la valeur de chaque paramètre. La liaison de la source se fait directement sur les paramètres de la méthode de test.
Avec JUnit 4, dans une même classe, une et une seule méthode d'usine doit être déclarée avec @Parameters
.
Avec JUnit 5, cette limitation est levée: plusieurs méthodes peuvent en effet être utilisées comme méthode d'usine.
Ainsi, à l'intérieur de la classe, nous pouvons ainsi déclarer certaines méthodes de test annotées avec @MethodSource("..")
qui font référence à différentes méthodes d'usine.
Par exemple, voici un exemple de classe de test qui affirme certains calculs d'addition:
import Java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.api.Assertions;
public class ParameterizedMethodSourceWithArgumentsTest {
@ParameterizedTest
@MethodSource("addFixture")
void add(int a, int b, int result) {
Assertions.assertEquals(result, a + b);
}
private static Stream<Arguments> addFixture() {
return Stream.of(
Arguments.of(1, 2, 3),
Arguments.of(4, -4, 0),
Arguments.of(-3, -3, -6));
}
}
Pour mettre à niveau les tests paramétrés existants de JUnit 4 vers JUnit 5, @MethodSource
Est un candidat à considérer.
Résumer
@MethodSource
A quelques points forts mais aussi quelques points faibles.
De nouvelles façons de spécifier les sources des tests paramétrés ont été introduites dans JUnit 5.
Voici quelques informations supplémentaires (loin d'être exhaustives) à leur sujet qui, je l'espère, pourraient donner une idée générale de la manière de traiter de manière générale.
Introduction
JUnit 5 introduit fonction de tests paramétrés en ces termes:
Les tests paramétrés permettent d'exécuter un test plusieurs fois avec différents arguments. Ils sont déclarés exactement comme les méthodes
@Test
Normales mais utilisent l'annotation@ParameterizedTest
À la place. De plus, vous devez déclarer au moins une source qui fournira les arguments pour chaque appel.
Exigence de dépendance
La fonction de tests paramétrés n'est pas incluse dans la dépendance de base junit-jupiter-engine
.
Vous devez ajouter une dépendance spécifique pour l'utiliser: junit-jupiter-params
.
Si vous utilisez Maven, voici la dépendance à déclarer:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.0.0</version>
<scope>test</scope>
</dependency>
Sources disponibles pour créer des données
Contrairement à JUnit 4, JUnit 5 fournit plusieurs saveurs et artefacts pour écrire des tests paramétrés
Les moyens de favoriser dépendent généralement de la source de données que vous souhaitez utiliser.
Voici les types de sources proposés par le framework et décrits dans la documentation :
@ValueSource
@EnumSource
@MethodSource
@CsvSource
@CsvFileSource
@ArgumentsSource
Voici les 3 principales sources que j'utilise réellement avec JUnit 5 et je vais vous présenter:
@MethodSource
@ValueSource
@CsvSource
Je les considère aussi basiques que j'écris des tests paramétrés. Ils devraient permettre d'écrire dans JUnit 5, le type de tests JUnit 4 que vous avez décrit.@EnumSource
, @ArgumentsSource
Et @CsvFileSource
Peuvent bien sûr être utiles mais ils sont plus spécialisés.
Présentation de @MethodSource
, @ValueSource
Et @CsvSource
1) @MethodSource
Ce type de source nécessite de définir une méthode d'usine.
Mais il offre également une grande flexibilité.
Dans JUnit 5, c'est la façon d'écrire les tests paramétrés les plus proches de JUnit 4.
Si vous avez un paramètre de méthode unique dans la méthode de test et que vous souhaitez utiliser tout type comme source, @MethodSource
est un très bon candidat.
Pour y parvenir, définissez une méthode qui renvoie un flux de la valeur pour chaque cas et annotez la méthode de test avec @MethodSource("methodName")
où methodName
est le nom de cette méthode de source de données .
Par exemple, vous pourriez écrire:
import Java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class ParameterizedMethodSourceTest {
@ParameterizedTest
@MethodSource("getValue_is_never_null_fixture")
void getValue_is_never_null(Foo foo) {
Assertions.assertNotNull(foo.getValue());
}
private static Stream<Foo> getValue_is_never_null_fixture() {
return Stream.of(new CsvFoo(), new SqlFoo(), new XmlFoo());
}
}
Si vous avez plusieurs paramètres de méthode dans la méthode de test et que vous souhaitez utiliser tout type comme source, @MethodSource
est également un très bon candidat.
Pour y parvenir, définissez une méthode qui renvoie un flux de org.junit.jupiter.params.provider.Arguments
Pour chaque cas à tester.
Par exemple, vous pourriez écrire:
import Java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.api.Assertions;
public class ParameterizedMethodSourceWithArgumentsTest {
@ParameterizedTest
@MethodSource("getFormatFixture")
void getFormat(Foo foo, String extension) {
Assertions.assertEquals(extension, foo.getExtension());
}
private static Stream<Arguments> getFormatFixture() {
return Stream.of(
Arguments.of(new SqlFoo(), ".sql"),
Arguments.of(new CsvFoo(), ".csv"),
Arguments.of(new XmlFoo(), ".xml"));
}
}
2) @ValueSource
Si vous avez un paramètre de méthode unique dans la méthode de test et que vous pouvez représenter la source du paramètre de l'un des ces types intégrés (String, int, long, double) , @ValueSource
conviennent.
@ValueSource
Définit en effet ces attributs:
String[] strings() default {};
int[] ints() default {};
long[] longs() default {};
double[] doubles() default {};
Vous pouvez par exemple l'utiliser de cette manière:
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
public class ParameterizedValueSourceTest {
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void sillyTestWithValueSource(int argument) {
Assertions.assertNotNull(argument);
}
}
Attention 1) vous ne devez pas spécifier plus d'un attribut d'annotation.
Attention 2) Le mappage entre la source et le paramètre de la méthode peut se faire entre deux types distincts.
Le type String
utilisé comme source de données permet notamment, grâce à son analyse, d'être converti en plusieurs autres types.
3) @CsvSource
Si vous avez plusieurs paramètres de méthode dans la méthode de test, un @CsvSource
Peut convenir.
Pour l'utiliser, annotez le test avec @CsvSource
Et spécifiez dans un tableau de String
chaque cas.
Les valeurs de chaque cas sont séparées par une virgule.
Comme @ValueSource
, Le mappage entre la source et le paramètre de la méthode peut se faire entre deux types distincts.
Voici un exemple qui illustre cela:
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
public class ParameterizedCsvSourceTest {
@ParameterizedTest
@CsvSource({ "12,3,4", "12,2,6" })
public void divideTest(int n, int d, int q) {
Assertions.assertEquals(q, n / d);
}
}
@CsvSource
VS @MethodSource
Ces types de source répondent à une exigence très classique: mappage de la source vers plusieurs paramètres de méthode dans la méthode de test.
Mais leur approche est différente.
@CsvSource
Présente certains avantages: il est plus clair et plus court.
En effet, les paramètres sont définis juste au-dessus de la méthode testée, pas d'obligation de créer une méthode de luminaire pouvant en plus générer des avertissements "non utilisés".
Mais il a également une limitation importante concernant les types de mappage.
Vous devez fournir un tableau de String
. Le cadre fournit des fonctionnalités de conversion mais il est limité.
Pour résumer, alors que le String
fourni comme source et les paramètres de la méthode de test ont le même type (String
-> String
) ou reposent sur une conversion intégrée (String
-> int
par exemple), @CsvSource
apparaît comme mode d'utilisation.
Comme ce n'est pas le cas, vous devez faire un choix entre conserver la flexibilité de @CsvSource
En créant un convertisseur personnalisé (sous-classe ArgumentConverter
) pour les conversions non effectuées par le framework ou en utilisant @MethodSource
avec une méthode d'usine qui renvoie Stream<Arguments>
.
Il présente les inconvénients décrits ci-dessus, mais il présente également le grand avantage de mapper tout type de boîtier, de la source aux paramètres.
Conversion d'arguments
Concernant le mappage entre la source (@CsvSource
Ou @ValueSource
Par exemple) et les paramètres de la méthode de test, comme vu, le framework permet de faire quelques conversions si les types ne sont pas les mêmes.
Ici est une présentation des deux types de conversions:
3.13.3. Conversion d'arguments
Conversion implicite
Pour prendre en charge des cas d'utilisation tels que
@CsvSource
, JUnit Jupiter fournit un certain nombre de convertisseurs de type implicites intégrés. Le processus de conversion dépend du type déclaré de chaque paramètre de méthode......
String
les instances sont actuellement implicitement converties en types de cible suivants.Target Type | Example boolean/Boolean | "true" → true byte/Byte | "1" → (byte) 1 char/Character | "o" → 'o' short/Short | "1" → (short) 1 int/Integer | "1" → 1 .....
Par exemple dans l'exemple précédent, une conversion implicite est effectuée entre String
depuis la source et int
définie comme paramètre:
@CsvSource({ "12,3,4", "12,2,6" })
public void divideTest(int n, int d, int q) {
Assertions.assertEquals(q, n / d);
}
Et ici, une conversion implicite est effectuée du paramètre String
source vers le paramètre LocalDate
:
@ParameterizedTest
@ValueSource(strings = { "2018-01-01", "2018-02-01", "2018-03-01" })
void testWithValueSource(LocalDate date) {
Assertions.assertTrue(date.getYear() == 2018);
}
Si pour deux types, aucune conversion n'est fournie par le framework, ce qui est le cas pour les types personnalisés, vous devez utiliser un ArgumentConverter
.
Conversion explicite
Au lieu d'utiliser la conversion d'argument implicite, vous pouvez explicitement spécifier un
ArgumentConverter
à utiliser pour un certain paramètre en utilisant l'annotation@ConvertWith
Comme dans l'exemple suivant.
JUnit fournit une implémentation de référence pour les clients qui ont besoin de créer un ArgumentConverter
spécifique.
Les convertisseurs d'arguments explicites sont destinés à être implémentés par les auteurs de tests. Ainsi, junit-jupiter-params ne fournit qu'un seul convertisseur d'arguments explicites qui peut également servir d'implémentation de référence:
JavaTimeArgumentConverter
. Il est utilisé via l'annotation composéeJavaTimeConversionPattern
.
Méthode de test utilisant ce convertisseur:
@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
assertEquals(2017, argument.getYear());
}
JavaTimeArgumentConverter
classe de convertisseur:
package org.junit.jupiter.params.converter;
import Java.time.LocalDate;
import Java.time.LocalDateTime;
import Java.time.LocalTime;
import Java.time.OffsetDateTime;
import Java.time.OffsetTime;
import Java.time.Year;
import Java.time.YearMonth;
import Java.time.ZonedDateTime;
import Java.time.chrono.ChronoLocalDate;
import Java.time.chrono.ChronoLocalDateTime;
import Java.time.chrono.ChronoZonedDateTime;
import Java.time.format.DateTimeFormatter;
import Java.time.temporal.TemporalQuery;
import Java.util.Collections;
import Java.util.LinkedHashMap;
import Java.util.Map;
import org.junit.jupiter.params.support.AnnotationConsumer;
/**
* @since 5.0
*/
class JavaTimeArgumentConverter extends SimpleArgumentConverter
implements AnnotationConsumer<JavaTimeConversionPattern> {
private static final Map<Class<?>, TemporalQuery<?>> TEMPORAL_QUERIES;
static {
Map<Class<?>, TemporalQuery<?>> queries = new LinkedHashMap<>();
queries.put(ChronoLocalDate.class, ChronoLocalDate::from);
queries.put(ChronoLocalDateTime.class, ChronoLocalDateTime::from);
queries.put(ChronoZonedDateTime.class, ChronoZonedDateTime::from);
queries.put(LocalDate.class, LocalDate::from);
queries.put(LocalDateTime.class, LocalDateTime::from);
queries.put(LocalTime.class, LocalTime::from);
queries.put(OffsetDateTime.class, OffsetDateTime::from);
queries.put(OffsetTime.class, OffsetTime::from);
queries.put(Year.class, Year::from);
queries.put(YearMonth.class, YearMonth::from);
queries.put(ZonedDateTime.class, ZonedDateTime::from);
TEMPORAL_QUERIES = Collections.unmodifiableMap(queries);
}
private String pattern;
@Override
public void accept(JavaTimeConversionPattern annotation) {
pattern = annotation.value();
}
@Override
public Object convert(Object input, Class<?> targetClass) throws ArgumentConversionException {
if (!TEMPORAL_QUERIES.containsKey(targetClass)) {
throw new ArgumentConversionException("Cannot convert to " + targetClass.getName() + ": " + input);
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
TemporalQuery<?> temporalQuery = TEMPORAL_QUERIES.get(targetClass);
return formatter.parse(input.toString(), temporalQuery);
}
}