Mécanisme d’extension pour JUnit 5

Le mécanisme d’extension de JUnit 5 est plus simple et plus pratique que ceux de JUnit 4. Il remplace à la fois @RunWith, @Rule et @ClassRule.

Utiliser une extension

JUnit a deux annotations pour appliquer une extension sur une classe de test, @ExtendWith et @RegisterExtension.

@ExtendWith s’utilise habituellement sur la classe de test, comme @RunWith de JUnit 4. La première différence, c’est qu’en JUnit 5, il est possible d’utiliser plusieurs extensions, en passant un tableau à l’annotation ou en l’utilisant plusieurs fois (elle est repeatable). La deuxième différence, c’est qu’elle peut s’utiliser sur un méthode de test, un champ ou un paramètre de méthode.

@ExtendWith(LoggingExtension.class)
public class SomeTest {
  //...
}

En remplacement de @ExtendWith, on peut aussi utiliser une annotation dédiée, elle-même annotée par @ExtendWith. Cette annotation peut être fournie avec l’extension ou faite maison.

@Inherited
@Retention(RUNTIME)
@ExtendWith(LoggingExtension.class)
public @interface Logging {
}
@Logging
public class SomeTest {
  //...
}

@RegisterExtension s’utilise uniquement sur un champ, comme @Rule ou @ClassRule de JUnit 4. Sa valeur ajoutée par rapport à @ExtendWith, c’est qu’on instancie nous-même l’extension, avec la possibilité de choisir des paramètres d’initialisation.

public class SomeTest {
  @RegisterExtension
  LoggingExtension loggingExtension = LoggingExtension.named("@RegisterExtension");

  //...
}

Extensions par défaut

Les extensions ci-dessous sont activées systématiquement. Elles permettent d’utiliser des annotations ou d’injecter des paramètres aux méthodes de tests ou du cycle de vie.

  • DisabledCondition : désactive les tests annotés avec @Disabled

  • TempDirectory : création de répertoire temporaire avec @TempDir

  • TimeoutExtension : applique un délai d’exécution aux tests annotés avec @Timeout

  • RepeatedTestExtension : exécute de façon répétée les tests annotés avec @RepeatedTest

  • TestInfoParameterResolver : injecte les paramètres de tests de type TestInfo

  • TestReporterParameterResolver : injecte les paramètres de tests de type TestReporter

Extensions fournies

En plus des extensions activée par défaut, les extensions ci-dessous sont fournies par JUnit et utilisables dans nos tests. Elles sont généralement utilisées sous la forme d’une annotation dédiée.

La plus grande famille concerne les annotations conditionnelles.

  • @DisabledForJreRange, @EnabledForJreRange

  • @DisabledIfEnvironmentVariable, @EnabledIfEnvironmentVariable

  • @DisabledIfSystemProperty, @EnabledIfSystemProperty

  • @EnabledForJreRange, @DisabledForJreRange

  • @DisabledOnOs, @EnabledOnOs`

  • @DisabledIf, @EnabledIf

  @Test
  @DisabledIf("tooLate")
  public void testIf() {
    System.out.println("Enabled at hour: " + LocalTime.now());
  }

  private boolean tooLate() {
    return LocalTime.now()
              .isAfter(LocalTime.of(22, 0));
  }
Les conditions peuvent être utilisées sur les méthodes du cycle de vie, pour initialiser le contexte du test en fonction de l’environnement.

La plus utilisée permet d’exécuter des tests paramétrés. Elle est dans l’artéfact org.junit.jupiter:junit-jupiter-params.

  • @ParameterizedTest

Extensions tierce

  • MockitoExtension, qui peut aussi être utilisé via l’annotation @MockitoSettings

  • @Testcontainers, qui ne peut pas être remplacée par @ExtendWith car l’extension vérifie la présence de l’annotation

  • SpringExtension, avec les annotations @SpringJUnitConfig, @SpringJUnitWebConfig, @SpringBootTest

Développer une extension

Une extension est une classe qui implémente l’interface Extension. Cette interface n’est qu’un marqueur puisqu’elle est vide. Concrètement, il faut implémenter une de ses sous-interface qui s’intègre dans le cycle de vie de JUnit.

Extension de cycle de vie

Ces extensions permettent de s’intégrer dans le cycle de vie du test et d’y ajouter des comportements. Par exemple, il est classique de démarrer un conteneur de composants ou un serveur avant le démarrage des tests. On peut aussi faire des extensions qui traitent certains types d’exceptions ou qui restituent les résultats de façon personnalisée.

  • BeforeAllCallback, AfterAllCallback

  • BeforeEachCallback, AfterEachCallback

  • BeforeTestExecutionCallback, AfterTestExecutionCallback

  • TestExecutionExceptionHandler

  • TestInstancePostProcessor

  • TestInstancePreDestroyCallback

Exemple :

public class SpringExtension
    implements BeforeAllCallback, AfterAllCallback,
               TestInstancePostProcessor,
               BeforeEachCallback, AfterEachCallback,
               BeforeTestExecutionCallback, AfterTestExecutionCallback,
               ParameterResolver {
  //...
}

Fournisseur de paramètres

Même sans faire des tests paramétrés, on peut déclarer des paramètres à une méthode de test simple. L’injection de ces paramètre est prise en charge par des extensions de type ParameterResolver. C’est aussi valable pour les méthodes du cycle de vie.

Dans l’exemple ci-dessous, la méthode de préparation prend un Instant en paramètre. Cet Instant est injecté par le InstantParameterResolver.

@ExtendWith(InstantParameterResolver.class)
public class ParameterTest {
  @BeforeEach
  void setUp(Instant instant) {
    System.out.println("@BeforeEach " + instant);
  }
  //...
}
public class InstantParameterResolver implements ParameterResolver {
  @Override
  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
      throws ParameterResolutionException {
    return parameterContext.getParameter().getType() == Instant.class;
  }

  @Override
  public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
      throws ParameterResolutionException {
    return Instant.now();
  }
}

Les paramètres de type TestInfo et TestReporter sont gérés nativement par JUnit grâce à des extensions par défaut.

Fabrique de test

La catégorie d’extension qui est probablement la moins utilisée est TestInstanceFactory qui permet d’instancier le test de façon programmatique.

Ça permet d’instancier une sous-classe du test ou de passer des paramètres au constructeur sans avoir recours à un ParameterResolver. L’exemple avec l'`Instant` donnerait ceci avec une fabrique :

public class FactoryTest {

  @RegisterExtension
  static TestInstanceFactory factory
      = (factoryContext, extensionContext) -> new FactoryTest(Instant.now().minus(1, ChronoUnit.DAYS));

  private final Instant instant;

  public FactoryTest(Instant now) {
    this.instant = now;
  }

  @Test
  void should_work() {
    System.out.println("@Test " + instant);
  }
}

Synthèse

L’utilisation d’extensions par l’annotation @ExtendWith est la plus évidente, mais plusieurs autres usages sont masqués. Ainsi, les annotations @Timeout ou @Disabled viennent d’annotations activées par défaut. Les annotations de condition, @DisabledXxx et @EnabledXxx sont des méta-annotations d’extensions.

Par ailleurs, si l’intérêt d’activer globalement une extension, via le fichier org.junit.jupiter.api.extension.Extension dans le répertoire /META-INF/services, ne parait pas évident dans le cas général, on comprend plus facilement lorsqu’il s’agit de résolveur de paramètre ou du support d’une annotation personnalisée.

Bref, les extensions de JUnit 5 sont plus puissantes et bien plus pratiques que ce qui existait dans les versions précédentes.