Exécuter des tests Cucumber dans des contextes Spring séparés

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.

arborescence du projet

Mise en place

Les différents paquets nécessaires sont :

  • io.cucumber.cucumber-java : l’implémentation Cucumber en Java
  • io.cucumer.cucumber-junit: pouvoir exécuter les tests Cucumber via JUnit Vintage
  • org.junit.vintage.junit-vintage-engine: JUnit vintage
  • io.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 :

organisation des packages dans la partie test

La structure est similaire pour les deux contextes :

structure de chaque contexte

  • 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 @Primaryde 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.
  • @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 :

tests_intellij

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 :

intellij_cucumber

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.