web-dev-qa-db-fra.com

Comment implémenter des tests paramétrés JUnit 4 dans JUnit 5?

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(){
       ...
   } 
}
22
Sled

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 un Stream, 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")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ée JavaTimeConversionPattern.

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);
    }

}
14
davidxxx