Dans cet article, nous allons voir comment configurer Cucumber pour exécuter des tests avec JUnit Vintage dans des contextes Spring différents.
Pourquoi utiliser des contextes différents ?
Dans notre cas, ce qui va caractériser un contexte Spring ce sont surtout les Beans qui le composent. Un contexte est donc un ensemble de Beans qui seront partagés pour un ensemble de tests Cucumber lors d’une exécution.
L’utilisation de contextes Spring séparés est intéressante pour disposer de différentes configurations de Beans. Cela nous permet de choisir l’implémentation des Beans instanciés durant un test afin de maîtriser le contexte d’exécution. Ce contexte d’exécution sera différent d’un test à l’autre.
Présentation du projet d’exemple
J’ai créé un projet qui servira à illustrer la configuration à mettre en place et qui vous permettra d’avoir une solution fonctionnelle. Vous trouverez les sources sur GitHub.
Dans ce projet se trouvent deux contextes différents (au sens bounded context) : printing et reporting.
Mise en place
Les différents paquets nécessaires sont :
io.cucumber.cucumber-java
: l’implémentation Cucumber en Javaio.cucumer.cucumber-junit
: pouvoir exécuter les tests Cucumber via JUnit Vintageorg.junit.vintage.junit-vintage-engine
: JUnit vintageio.cucumber.cucumber-spring
: charge le contexte Spring lors d’un test Cucumber
Dans le pom.xml :
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.8.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-bom</artifactId>
<version>7.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Dans la partie test du projet, nous séparons nos features de la même manière :
La structure est similaire pour les deux contextes :
- Une configuration de Beans pour les tests (annotée avec
@TestConfiguration
). Cette classe permettra soit de déclarer de nouveaux Beans qui ne serviront que dans les tests, soit de remplacer des Beans de la configuration de production avec une autre implémentation (par exemple un mock). On reconnaît ces Beans grâce à l’annotation@Primary
de Spring. - Une classe qui contient les implémentations de nos étapes de scénario Gherkin (dans un projet réel on trouverait plus d’une classe)
- Une classe qui fait la configuration de Cucumber et la liaison avec Spring
La partie la plus importante se trouve dans la classe de configuration, par exemple pour le contexte “reporting” :
package separated_contexts.features.reporting;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import io.cucumber.spring.CucumberContextConfiguration;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
@RunWith(Cucumber.class)
@CucumberOptions(
glue = "separated_contexts.features.reporting",
features = "classpath:separated_contexts/features/reporting")
@CucumberContextConfiguration
@SpringBootTest(classes = ReportingSpringTestConfig.class)
public class RunReportCucumberTests {
}
@RunWith(Cucumber.class)
: indique de lancer les tests Cucumber via JUnit Vintage@CucumberOptions(glue = "separated_contexts.features.reporting",features = "classpath:separated_contexts/features/reporting")
:- la propriété
glue
indique le package où trouver l’implémentation des steps, les hooks et les plugins. - La propriété
features
indique le dossier où se trouvent les fichiers de features (.feature
). Cette annotation permet déjà en elle-même de séparer les tests Cucumber en différentes exécutions.
- la propriété
@CucumberContextConfiguration
et@SpringBootTest(classes = ReportingSpringTestConfig.class)
: la première annotation indique à Cucumber quelle configuration Spring utiliser via la deuxième annotation. Dans notre cas, nous indiquons cela via l’annotation de Spring Boot@SpringBootTest
mais cela fonctionne avec les autres annotations de configuration de Spring telles que@Configuration
. Avec la propriétéclasses
nous indiquons à Spring d’utiliser notre configuration de Test. Nous aurions pu ne rien préciser si la configuration Spring de production suffisait à nos tests. Plus d’informations sont disponibles sur la documentation du projet disponible dans le repository GitHub.
Exécution des tests
Lorsqu’on exécute tous les tests via Intellij ou via Maven, on voit qu’ils sont lancés dans des sessions différentes :
Exécution des tests avec IntelliJ et limitations
Depuis Intellij, si vous exécutez les tests Cucumber via la classe de configuration (c’est-à-dire la classe RunPrintingCucumberTests
ou la classe RunReportCucumberTests
dans notre projet d’exemple) il n’y aura pas de problème, par contre si vous exécutez un scénario directement depuis un fichier de feature (via le bouton d’action vert dans la marge ), le lancement échouera avec un message similaire à :
io.cucumber.core.backend.CucumberBackendException: Glue class class
separated_contexts.features.printing.RunPrintingCucumberTests$CucumberSpringPrinting
and class
separated_contexts.features.reporting.RunReportCucumberTests$CucumberSpringReporting
are both annotated with @CucumberContextConfiguration.
Please ensure only one class configures the spring context
Cela vient du fait que par défaut, Intellij va scanner tous les packages afin de trouver toutes les classes de glue Cucumber. On se retrouve donc avec deux classes de glue CucumberSpringPrinting
et CucumberSpringReporting
toutes deux annotées avec @CucumberContextConfiguration
ce qui n’est pas autorisé par le framework Cucumber JVM.
Il est possible de remédier à moitié à ce problème en éditant la configuration de tests générée par IntelliJ et de retirer du champ Glue les packages en trop :
Alternative via les tags
Une autre alternative si on souhaite exécuter un test ou plusieurs tests en particulier est de les annoter avec un tag Cucumber est d’indiquer ce tag sur l’annotation @CucumberOptions
.
afficher.feature
:
#language: fr
Fonctionnalité: Fait quelque chose
@afficher
Scénario: Quelque chose est fait
Quand l'affichage de "bonjour" est fait
Alors "bonjour" est affiché
Scénario: Autre scénario non lancé car sans tag
Quand l'affichage de "non lancé" est fait
Alors "non lancé" est affiché
Classe RunReportCucumberTests :
package separated_contexts.features.printing;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import io.cucumber.spring.CucumberContextConfiguration;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
@RunWith(Cucumber.class)
@CucumberOptions(
tags = "@afficher",
glue = "separated_contexts.features.printing",
features = "classpath:separated_contexts/features/printing")
@CucumberContextConfiguration
@SpringBootTest(classes = PrintingSpringTestConfig.class)
public class RunPrintingCucumberTests {
}
L’idée est d’utiliser cette option durant le développement uniquement. Attention à ne pas la laisser définitivement car tous les tests ne seraient pas exécutés lors de la phase test
de Maven (important pour les tests lancés par le pipeline).
Exécution via Maven
Lors de l’exécution de la commande mvn test
, c’est le plugin Maven Surfire qui s’occupe d’exécuter les tests unitaires de la solution.
Dans notre cas, nous avons fait en sorte d’exécuter les tests Cucumber via JUnit Vintage grâce à l’annotation @RunWith(Cucumber.class)
, ils sont donc pris en compte lors du goal test
de Maven. C’est ensuite le runner Cucumber qui prend le relai pour lancer les tests Cucumber. Les classes RunPrintingCucumberTests
et RunReportCucumberTests
sont détectées et comme elles contiennent l’annotation @CucumberOptions
indiquant où trouver les fichiers feature et le glue code, les tests sont correctement exécutés en isolation :
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running separated_contexts.features.printing.RunPrintingCucumberTests
Message 'bonjour' was printed.
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.058 s - in separated_contexts.features.printing.RunPrintingCucumberTests
[INFO] Running separated_contexts.features.reporting.RunReportCucumberTests
Template de test 'mon message'
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.17 s - in separated_contexts.features.reporting.RunReportCucumberTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
Conclusion
Dans cet article, nous avons vu qu’il était possible de lancer des tests Cucumber avec des contextes Spring différents. Ce type de configuration est très utile lorsqu’on souhaite que nos Beans aient un comportement différent d’un test à l’autre. Lors de l’écriture d’un test, cela permet de se concentrer sur les Beans qui sont pertinents dans notre contexte et de ne pas devoir s’occuper des éléments d’un autre contexte.