Injecter des paramètres dans les tests unitaires JUnit Jupiter

Injecter des paramètres dans les tests unitaires JUnit Jupiter

1. Overview

Avant JUnit 5, pour introduire une nouvelle fonctionnalité intéressante, l'équipe JUnit devait le faire dans l'API principale. Avec JUnit 5, l’équipe a décidé qu’il était temps de repousser les possibilités d’extension de l’API JUnit principale en dehors de JUnit lui-même, une philosophie de base de JUnit 5 appelée «https://github.com/junit-team/junit5/wiki/Core-Principles [Préférez les points d’extension aux fonctionnalités] ».

Dans cet article, nous allons nous concentrer sur l'une de ces interfaces de point d'extension -ParameterResolver - que vous pouvez utiliser pour injecter des paramètres dans vos méthodes de test. Il existe plusieurs façons de rendre la plateforme JUnit consciente de votre extension (un processus appelé «enregistrement»), et dans cet article, nous allons nous concentrer sur l'enregistrement dedeclarative (c'est-à-dire l'enregistrement via le code source) .

2. ParameterResolver

L'injection de paramètres dans vos méthodes de test peut être réalisée à l'aide de l'API JUnit 4, mais elle est assez limitée. Avec JUnit 5, l'API Jupiter peut être étendue - en implémentantParameterResolver - pour servir des objets de tout type à vos méthodes de test. Regardons.

2.1. FooParameterResolver

public class FooParameterResolver implements ParameterResolver {
  @Override
  public boolean supportsParameter(ParameterContext parameterContext,
    ExtensionContext extensionContext) throws ParameterResolutionException {
      return parameterContext.getParameter().getType() == Foo.class;
  }

  @Override
  public Object resolveParameter(ParameterContext parameterContext,
    ExtensionContext extensionContext) throws ParameterResolutionException {
      return new Foo();
  }
}

Tout d'abord, nous devons implémenterParameterResolver – qui a deux méthodes:

  • supportsParameter() - renvoie true si le type du paramètre est pris en charge (Foo dans cet exemple), et

  • resolveParamater() - sert un objet du type correct (une nouvelle instance Foo dans cet exemple), qui sera ensuite injecté dans votre méthode de test

2.2. FooTest

@ExtendWith(FooParameterResolver.class)
public class FooTest {
    @Test
    public void testIt(Foo fooInstance) {
        // TEST CODE GOES HERE
    }
}

Ensuite, pour utiliser l'extension, nous devons la déclarer - c'est-à-dire en informer la plate-forme JUnit - via l'annotation@ExtendWith (ligne 1).

Lorsque la plate-forme JUnit exécute votre test unitaire, elle récupère une instanceFoo deFooParameterResolver et la transmet à la méthodetestIt() (ligne 4).

L’extension a unscope of influence, qui active l’extension, en fonction duwhere déclaré.

L'extension peut être soit active au:

  • niveau de la méthode, où il est actif uniquement pour cette méthode, ou

  • niveau de la classe, où il est actif pour toute la classe de test, ou classe de test@Nested comme nous le verrons bientôt

Note: you should not declare a ParameterResolverat both scopes for the same parameter type, or the JUnit Platform will complain about this ambiguity.

Pour cet article, nous verrons comment écrire et utiliser deux extensions pour injecter des objetsPerson: une qui injecte de «bonnes» données (appeléesValidPersonParameterResolver) et une qui injecte de «mauvaises» données (InvalidPersonParameterResolver). Nous utiliserons ces données pour tester l'unité une classe appeléePersonValidator, qui valide l'état d'un objetPerson.

3. Write the Extensions

Maintenant que nous comprenons ce qu'est une extensionParameterResolver, nous sommes prêts à écrire:

  • un qui fournit des objetsvalidPerson (ValidPersonParameterResolver), et

  • celui qui fournit des objetsinvalidPerson (InvalidPersonParameterResolver)

3.1. ValidPersonParameterResolver

public class ValidPersonParameterResolver implements ParameterResolver {

  public static Person[] VALID_PERSONS = {
      new Person().setId(1L).setLastName("Adams").setFirstName("Jill"),
      new Person().setId(2L).setLastName("Baker").setFirstName("James"),
      new Person().setId(3L).setLastName("Carter").setFirstName("Samanta"),
      new Person().setId(4L).setLastName("Daniels").setFirstName("Joseph"),
      new Person().setId(5L).setLastName("English").setFirstName("Jane"),
      new Person().setId(6L).setLastName("Fontana").setFirstName("Enrique"),
  };

Notez le tableauVALID_PERSONS d'objetsPerson. Il s'agit du référentiel des objetsPerson valides parmi lesquels un sera choisi au hasard à chaque fois que la méthoderesolveParameter() sera appelée par la plateforme JUnit.

Avoir les objets Personne valides ici accomplit deux choses:

  1. Séparation des problèmes entre le test unitaire et les données qui le pilotent

  2. Réutiliser, si d'autres tests unitaires nécessitent des objetsPerson valides pour les piloter

@Override
public boolean supportsParameter(ParameterContext parameterContext,
  ExtensionContext extensionContext) throws ParameterResolutionException {
    boolean ret = false;
    if (parameterContext.getParameter().getType() == Person.class) {
        ret = true;
    }
    return ret;
}

Si le type de paramètre estPerson, alors l'extension indique à la plate-forme JUnit qu'elle prend en charge ce type de paramètre, sinon elle renvoie false, en disant que non.

Pourquoi cela devrait-il être important? Bien que les exemples de cet article soient simples, dans une application réelle, les classes de tests unitaires peuvent être très volumineuses et complexes, avec de nombreuses méthodes de test prenant en compte différents types de paramètres. La plate-forme JUnit doit vérifier avec tous lesParameterResolver enregistrés lors de la résolution des paramètreswithin the current scope of influence.

@Override
public Object resolveParameter(ParameterContext parameterContext,
  ExtensionContext extensionContext) throws ParameterResolutionException {
    Object ret = null;
    if (parameterContext.getParameter().getType() == Person.class) {
        ret = VALID_PERSONS[new Random().nextInt(VALID_PERSONS.length)];
    }
    return ret;
}

Un objet aléatoirePerson est renvoyé à partir du tableauVALID_PERSONS. Notez queresolveParameter() n'est appelé par la plate-forme JUnit que sisupportsParameter() renvoietrue.

3.2. InvalidPersonParameterResolver

public class InvalidPersonParameterResolver implements ParameterResolver {
  public static Person[] INVALID_PERSONS = {
      new Person().setId(1L).setLastName("Ad_ams").setFirstName("Jill,"),
      new Person().setId(2L).setLastName(",Baker").setFirstName(""),
      new Person().setId(3L).setLastName(null).setFirstName(null),
      new Person().setId(4L).setLastName("Daniel&").setFirstName("{Joseph}"),
      new Person().setId(5L).setLastName("").setFirstName("English, Jane"),
      new Person()/*.setId(6L).setLastName("Fontana").setFirstName("Enrique")*/,
  };

Notez le tableauINVALID_PERSONS d'objetsPerson. Tout comme avecValidPersonParameterResolver, cette classe contient un magasin de données «incorrectes» (c'est-à-dire non valides) à utiliser par des tests unitaires pour s'assurer, par exemple, quePersonValidator.ValidationExceptions sont correctement lancés en présence de données non valides :

@Override
public Object resolveParameter(ParameterContext parameterContext,
  ExtensionContext extensionContext) throws ParameterResolutionException {
    Object ret = null;
    if (parameterContext.getParameter().getType() == Person.class) {
        ret = INVALID_PERSONS[new Random().nextInt(INVALID_PERSONS.length)];
    }
    return ret;
}

@Override
public boolean supportsParameter(ParameterContext parameterContext,
  ExtensionContext extensionContext) throws ParameterResolutionException {
    boolean ret = false;
    if (parameterContext.getParameter().getType() == Person.class) {
        ret = true;
    }
    return ret;
}

Le reste de cette classe se comporte naturellement exactement comme son «bon» homologue.

4. Déclarer et utiliser les extensions

Maintenant que nous avons deuxParameterResolver, il est temps de les utiliser. Créons une classe de test JUnit pourPersonValidator appeléePersonValidatorTest.

Nous utiliserons plusieurs fonctionnalités disponibles uniquement dans JUnit Jupiter:

  • @DisplayName - c'est le nom qui apparaît sur les rapports de test, et bien plus lisible par l'homme

  • @Nested - crée une classe de test imbriquée, complète avec son propre cycle de vie de test, distincte de sa classe parente

  • @RepeatedTest - le test est répété le nombre de fois spécifié par l'attribut value (10 dans chaque exemple)

En utilisant les classes @Nested, nous sommes en mesure de tester les données valides et non valides dans la même classe de test, tout en les gardant complètement éloignées les unes des autres:

@DisplayName("Testing PersonValidator")
public class PersonValidatorTest {

    @Nested
    @DisplayName("When using Valid data")
    @ExtendWith(ValidPersonParameterResolver.class)
    public class ValidData {

        @RepeatedTest(value = 10)
        @DisplayName("All first names are valid")
        public void validateFirstName(Person person) {
            try {
                assertTrue(PersonValidator.validateFirstName(person));
            } catch (PersonValidator.ValidationException e) {
                fail("Exception not expected: " + e.getLocalizedMessage());
            }
        }
    }

    @Nested
    @DisplayName("When using Invalid data")
    @ExtendWith(InvalidPersonParameterResolver.class)
    public class InvalidData {

        @RepeatedTest(value = 10)
        @DisplayName("All first names are invalid")
        public void validateFirstName(Person person) {
            assertThrows(
              PersonValidator.ValidationException.class,
              () -> PersonValidator.validateFirstName(person));
        }
    }
}

Remarquez comment nous pouvons utiliser les extensionsValidPersonParameterResolver etInvalidPersonParameterResolver dans la même classe de test principale - en les déclarant uniquement au niveau de la classe @Nested. Essayez cela avec JUnit 4! (Alerte spoiler: vous ne pouvez pas le faire!)

5. Conclusion

Dans cet article, nous avons exploré comment écrire deux extensionsParameterResolver - pour servir des objets valides et non valides. Ensuite, nous avons vu comment utiliser ces deux implémentations deParameterResolver dans un test unitaire.

Comme toujours, le code est disponibleover on Github.

Et, si vous souhaitez en savoir plus sur le modèle d'extension JUnit Jupiter, consultez lesJUnit 5 User’s Guide oupart 2 of my tutorial on developerWorks.