Ecrire des modèles pour des cas de test à l'aide de JUnit 5

1. Vue d'ensemble

La bibliothèque JUnit 5 offre de nombreuses nouvelles fonctionnalités par rapport à ses versions précédentes. Une de ces fonctionnalités est les modèles de test. En bref, les modèles de test sont une puissante généralisation des tests paramétrés et répétés de JUnit 5.

Dans ce tutoriel, nous allons apprendre à créer un modèle de test à l'aide de JUnit 5.

2. Dépendances de Maven

Commençons par ajouter les dépendances à notre pom.xml .

Nous devons ajouter la dépendance principale JUnit 5 junit-jupiter-engine :

 org.junit.jupiter junit-jupiter-engine 5.7.0 

En plus de cela, nous devrons également ajouter la dépendance junit-jupiter-api :

 org.junit.jupiter junit-jupiter-api 5.7.0 

De même, nous pouvons ajouter les dépendances nécessaires à notre fichier build.gradle :

testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.0' testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.0'

3. L'énoncé du problème

Avant de regarder les modèles de test, jetons un bref coup d'œil aux tests paramétrés de JUnit 5. Les tests paramétrés nous permettent d'injecter différents paramètres dans la méthode de test. En conséquence, lors de l' utilisation de tests paramétrés, nous pouvons exécuter une seule méthode de test plusieurs fois avec différents paramètres.

Supposons que nous souhaitons maintenant exécuter notre méthode de test plusieurs fois - non seulement avec des paramètres différents, mais également sous un contexte d'appel différent à chaque fois.

En d'autres termes, nous aimerions que la méthode de test soit exécutée plusieurs fois, chaque appel utilisant une combinaison différente de configurations telles que:

  • en utilisant différents paramètres
  • préparer différemment l'instance de la classe de test - c'est-à-dire injecter différentes dépendances dans l'instance de test
  • exécution du test dans différentes conditions, telles que l'activation / la désactivation d'un sous-ensemble d'appels si l'environnement est « QA »
  • s'exécutant avec un comportement de rappel de cycle de vie différent - peut-être souhaitons-nous configurer et supprimer une base de données avant et après un sous-ensemble d'appels

L'utilisation de tests paramétrés s'avère rapidement limitée dans ce cas. Heureusement, JUnit 5 offre une solution puissante pour ce scénario sous la forme de modèles de test.

4. Modèles de test

Les modèles de test eux-mêmes ne sont pas des cas de test. Au lieu de cela, comme leur nom l'indique, ce ne sont que des modèles pour des cas de test donnés. Ils constituent une puissante généralisation des tests paramétrés et répétés.

Les modèles de test sont appelés une fois pour chaque contexte d'appel qui leur est fourni par le (s) fournisseur (s) de contexte d'appel.

Regardons maintenant un exemple des modèles de test. Comme nous l'avons établi ci-dessus, les principaux acteurs sont:

  • une méthode cible de test
  • une méthode de modèle de test
  • un ou plusieurs fournisseurs de contexte d'appel enregistrés avec la méthode de modèle
  • un ou plusieurs contextes d'appel fournis par chaque fournisseur de contexte d'appel

4.1. La méthode de la cible de test

Pour cet exemple, nous allons utiliser une méthode UserIdGeneratorImpl.generate simple comme cible de test.

Définissons la classe UserIdGeneratorImpl :

public class UserIdGeneratorImpl implements UserIdGenerator { private boolean isFeatureEnabled; public UserIdGeneratorImpl(boolean isFeatureEnabled) { this.isFeatureEnabled = isFeatureEnabled; } public String generate(String firstName, String lastName) { String initialAndLastName = firstName.substring(0, 1).concat(lastName); return isFeatureEnabled ? "bael".concat(initialAndLastName) : initialAndLastName; } }

La méthode generate , qui est notre cible de test, prend le firstName et le lastName comme paramètres et génère un identifiant utilisateur. Le format de l'ID utilisateur varie selon qu'un commutateur de fonction est activé ou non.

Voyons à quoi cela ressemble:

Given feature switch is disabled When firstName = "John" and lastName = "Smith" Then "JSmith" is returned Given feature switch is enabled When firstName = "John" and lastName = "Smith" Then "baelJSmith" is returned

Ensuite, écrivons la méthode du modèle de test.

4.2. La méthode du modèle de test

Voici un modèle de test pour notre méthode cible de test UserIdGeneratorImpl.generate :

public class UserIdGeneratorImplUnitTest { @TestTemplate @ExtendWith(UserIdGeneratorTestInvocationContextProvider.class) public void whenUserIdRequested_thenUserIdIsReturnedInCorrectFormat(UserIdGeneratorTestCase testCase) { UserIdGenerator userIdGenerator = new UserIdGeneratorImpl(testCase.isFeatureEnabled()); String actualUserId = userIdGenerator.generate(testCase.getFirstName(), testCase.getLastName()); assertThat(actualUserId).isEqualTo(testCase.getExpectedUserId()); } }

Examinons de plus près la méthode du modèle de test.

Pour commencer, nous créons notre méthode de modèle de test en la marquant avec l' annotation JUnit 5 @TestTemplate .

Ensuite, nous enregistrons un fournisseur de contexte , UserIdGeneratorTestInvocationContextProvider, à l'aide de l' annotation @ExtendWith . Nous pouvons enregistrer plusieurs fournisseurs de contexte avec le modèle de test. Cependant, aux fins de cet exemple, nous enregistrons un seul fournisseur.

En outre, la méthode de modèle reçoit une instance de UserIdGeneratorTestCase en tant que paramètre. Ceci est simplement une classe wrapper pour les entrées et le résultat attendu du cas de test:

public class UserIdGeneratorTestCase { private boolean isFeatureEnabled; private String firstName; private String lastName; private String expectedUserId; // Standard setters and getters }

Enfin, nous invoquons la méthode de la cible de test et affirmons que ce résultat est comme prévu

Il est maintenant temps de définir notre fournisseur de contexte d'appel .

4.3. Le fournisseur de contexte d'appel

Nous devons enregistrer au moins un TestTemplateInvocationContextProvider avec notre modèle de test. Chaque TestTemplateInvocationContextProvider enregistré fournit un Stream d' instances TestTemplateInvocationContext .

Previously, using the @ExtendWith annotation, we registered UserIdGeneratorTestInvocationContextProvider as our invocation provider.

Let's define this class now:

public class UserIdGeneratorTestInvocationContextProvider implements TestTemplateInvocationContextProvider { //... }

Our invocation context implements the TestTemplateInvocationContextProvider interface, which has two methods:

  • supportsTestTemplate
  • provideTestTemplateInvocationContexts

Let's start by implementing the supportsTestTemplate method:

@Override public boolean supportsTestTemplate(ExtensionContext extensionContext) { return true; }

The JUnit 5 execution engine calls the supportsTestTemplate method first to validate if the provider is applicable for the given ExecutionContext. In this case, we simply return true.

Now, let's implement the provideTestTemplateInvocationContexts method:

@Override public Stream provideTestTemplateInvocationContexts( ExtensionContext extensionContext) { boolean featureDisabled = false; boolean featureEnabled = true; return Stream.of( featureDisabledContext( new UserIdGeneratorTestCase( "Given feature switch disabled When user name is John Smith Then generated userid is JSmith", featureDisabled, "John", "Smith", "JSmith")), featureEnabledContext( new UserIdGeneratorTestCase( "Given feature switch enabled When user name is John Smith Then generated userid is baelJSmith", featureEnabled, "John", "Smith", "baelJSmith")) ); }

The purpose of the provideTestTemplateInvocationContexts method is to provide a Stream of TestTemplateInvocationContext instances. For our example, it returns two instances, provided by the methods featureDisabledContext and featureEnabledContext. Consequently, our test template will run twice.

Next, let's look at the two TestTemplateInvocationContext instances returned by these methods.

4.4. The Invocation Context Instances

The invocation contexts are implementations of the TestTemplateInvocationContext interface and implement the following methods:

  • getDisplayName – provide a test display name
  • getAdditionalExtensions – return additional extensions for the invocation context

Let's define the featureDisabledContext method that returns our first invocation context instance:

private TestTemplateInvocationContext featureDisabledContext(   UserIdGeneratorTestCase userIdGeneratorTestCase) { return new TestTemplateInvocationContext() { @Override public String getDisplayName(int invocationIndex) { return userIdGeneratorTestCase.getDisplayName(); } @Override public List getAdditionalExtensions() { return asList( new GenericTypedParameterResolver(userIdGeneratorTestCase), new BeforeTestExecutionCallback() { @Override public void beforeTestExecution(ExtensionContext extensionContext) { System.out.println("BeforeTestExecutionCallback:Disabled context"); } }, new AfterTestExecutionCallback() { @Override public void afterTestExecution(ExtensionContext extensionContext) { System.out.println("AfterTestExecutionCallback:Disabled context"); } } ); } }; }

Firstly, for the invocation context returned by the featureDisabledContext method, the extensions that we register are:

  • GenericTypedParameterResolver – a parameter resolver extension
  • BeforeTestExecutionCallback – a lifecycle callback extension that runs immediately before the test execution
  • AfterTestExecutionCallback – a lifecycle callback extension that runs immediately after the test execution

However, for the second invocation context, returned by the featureEnabledContext method, let's register a different set of extensions (keeping the GenericTypedParameterResolver):

private TestTemplateInvocationContext featureEnabledContext(   UserIdGeneratorTestCase userIdGeneratorTestCase) { return new TestTemplateInvocationContext() { @Override public String getDisplayName(int invocationIndex) { return userIdGeneratorTestCase.getDisplayName(); } @Override public List getAdditionalExtensions() { return asList( new GenericTypedParameterResolver(userIdGeneratorTestCase), new DisabledOnQAEnvironmentExtension(), new BeforeEachCallback() { @Override public void beforeEach(ExtensionContext extensionContext) { System.out.println("BeforeEachCallback:Enabled context"); } }, new AfterEachCallback() { @Override public void afterEach(ExtensionContext extensionContext) { System.out.println("AfterEachCallback:Enabled context"); } } ); } }; }

For the second invocation context, the extensions that we register are:

  • GenericTypedParameterResolver – a parameter resolver extension
  • DisabledOnQAEnvironmentExtension – an execution condition to disable the test if the environment property (loaded from the application.properties file) is “qa
  • BeforeEachCallback – a lifecycle callback extension that runs before each test method execution
  • AfterEachCallback – a lifecycle callback extension that runs after each test method execution

From the above example, it is clear to see that:

  • the same test method is run under multiple invocation contexts
  • each invocation context uses its own set of extensions that differ both in number and nature from the extensions in other invocation contexts

As a result, a test method can be invoked multiple times under a completely different invocation context each time. And by registering multiple context providers, we can provide even more additional layers of invocation contexts under which to run the test.

5. Conclusion

In this article, we looked at how JUnit 5's test templates are a powerful generalization of parameterized and repeated tests.

Pour commencer, nous avons examiné certaines limites des tests paramétrés. Ensuite, nous avons expliqué comment les modèles de test surmontent les limitations en permettant à un test d'être exécuté dans un contexte différent pour chaque appel.

Enfin, nous avons examiné un exemple de création d'un nouveau modèle de test. Nous avons décomposé l'exemple pour comprendre comment les modèles fonctionnent conjointement avec les fournisseurs de contexte d'appel et les contextes d'appel.

Comme toujours, le code source des exemples utilisés dans cet article est disponible à l'adresse over sur GitHub.