Dans cette article, nous allons développer une pipeline de CI pour une application Java, et publier des rapports de test et de couverture de test dans CodeBuild
.
CloudFormation
mvn test
) en Cucumber dans la pipelinemvn verify
) en Junitmvn verify
) en CucumberTable of contents generated with markdown-toc
Cette article est le 1e d'une série. Voir aussi:
Dans cette série d'articles, nous allons développer une application Java backend très simple, typiquement composée uniquement d'une classe renvoyant une String
en dur.
Cette application sera accompagnée d'une pipeline de CI/CD avec éxécution de tests unitaires et de tests automatisés, éxécutés sur l'application déployée sur une ou plusieurs instances EC2.
Les rapports d'éxécution et de couverture des tests seront publiés dans CodeBuild
Le but est d'avoir un point de départ réutilisable, fournissant un exemple simple, permettant de réimplémenter cette logique de pipeline et de publication de rapports de tests sur des projets plus compliqués.
Le but de la série d'articles est d'avoir au final:
mvn verify
) sont éxécutés lors de la phase de Build
de la pipelinemvn verify
) est publiémvn verify
) est publiéNous allons réaliser la cible par les étapes suivantes:
Dans cet article, nous allons réaliser l'étape n°1, à savoir:
mvn verify
), avec Junit et CucumberCloudFormation
tag de départ: 1.1-initial-commit
tag d'arrivée: 1.2-s3-bucket
Nous créons un bucket S3 avec le template CloudFormation
suivant:
Parameters:
ApplicationName:
Type: String
Description: Application Name
Resources:
S3Bucket:
Type: 'AWS::S3::Bucket'
Description: S3 bucket for pipeline artifacts
Properties:
BucketName: !Join
- '-'
- - !Ref 'AWS::Region'
- !Ref 'AWS::AccountId'
- !Ref ApplicationName
- bucket-pipeline
Nous créons aussi les 2 scripts suivants pour nous faciliter le travail d'installation et de destruction de l'infra:
create-all.sh
delete-all.sh
Nous pouvons créer le bucket S3 via la commande suivante:
./create-all.sh my-app
Vérifions la bonne éxécution création du bucket de la stack CloudFormation
:
Vérifions la création du bucket S3:
tag de départ: 1.2-s3-bucket
tag d'arrivée: 1.3-github-connection
Nous rajoutons la ressource CloudFormation
suivante pour créer la connexion Github
:
GithubConnection:
Type: AWS::CodeStarConnections::Connection
Properties:
ConnectionName: !Ref ApplicationName
ProviderType: GitHub
updatons la stack CloudFormation
via le script helper:
./create-all.sh my-app
On vérifie la bonne éxécution de l'update de la stack:
Et la présence de la connexion Github:
Les connexion github créées par CloudFormation
ou la CLI AWS sont toujours pending et doivent être activées à la main.
Pour autant que l'auteur le sache, il n'y a pas de moyen d'activer automatiquement une connexion (sauf bricolage avec quelque chose comme Sélénium éventuellement).
Voir https://docs.aws.amazon.com/dtconsole/latest/userguide/connections-update.html
A connection created through [...] AWS CloudFormation is in PENDING status by default [...]
You **must** use the console to update a pending connection. You cannot update a pending connection using the AWS CLI.
tag de départ: 1.3-github-connection
tag d'arrivée: 1.4-codebuild-iam-role
Dans cette étape, nous créons un rôle qui sera endossé par le futur projet CodeBuild
, ainsi que les permissions associées.
Ce qui se traduit par la ressource CloudFormation
suivante:
BuildProjectRole:
Type: 'AWS::IAM::Role'
Description: IAM role for !Ref ApplicationName build resource
Properties:
RoleName: !Join
- '-'
- - !Ref ApplicationName
- build-role
Path: /
Policies:
- PolicyName: !Join
- '-'
- - !Ref ApplicationName
- build-policy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketAcl
- s3:GetBucketLocation
Resource:
- !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}'
- !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}/*'
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*'
- Effect: Allow
Action:
- codebuild:CreateReportGroup
- codebuild:CreateReport
- codebuild:UpdateReport
- codebuild:BatchPutTestCases
- codebuild:BatchPutCodeCoverages
Resource: !Sub 'arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${ApplicationName}-*'
AssumeRolePolicyDocument:
Statement:
- Action: "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Analysons un peu ce que l'on vient de rajouter: Nous avons donc créé un "rôle", l'équivalent AWS d'une "carte d'accès", qui définit:
Voyons ce que cela donne dans notre rôle:
Action
, pour le bucket S3 que l'on a créé précédemmentCloudWatch logs
. On note que sans ces droits, le projet CodeBuild
ne pourra pas s'éxécuterCodeBuild
updatons la stack CloudFormation
via le script helper:
./create-all.sh my-app
On vérifie la bonne éxécution de la mise à jour de la stack, ainsi que la création de la nouvelle ressource:
On vérifie rapidement les permissions sur le rôle nouvellement créé:
tag de départ: 1.4-codebuild-iam-role
tag d'arrivée: 1.5-codepipeline-iam-role
Dans cette étape, nous créons un rôle qui sera endossé par le futur projet CodePipeline
, ainsi que les permissions associées.
Ce qui se traduit par la ressource CloudFormation
suivante:
PipelineRole:
Type: 'AWS::IAM::Role'
Description: IAM role for !Ref ApplicationName pipeline resource
Properties:
RoleName: !Join
- '-'
- - !Ref ApplicationName
- pipeline-role
Path: /
Policies:
- PolicyName: !Join
- '-'
- - !Ref ApplicationName
- pipeline-policy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- codestar-connections:UseConnection
Resource: !Ref GithubConnection
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketAcl
- s3:PutObjectAcl
- s3:GetBucketLocation
Resource:
- !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}'
- !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}/*'
AssumeRolePolicyDocument:
Statement:
- Action: "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
Ici aussi, analysons le rôle créé:
Github
définie dans l'étape #2Github
, mais c'est elle-même qui va récupérer le code source et le pousser dans le bucket s3, il n'y a pas de délégation de l'action vers un autre serviceCodePipeline
updatons la stack CloudFormation
via le script helper:
./create-all.sh my-app
On vérifie la bonne éxécution de la mise à jour de la stack, ainsi que la création de la nouvelle ressource:
On jette un oeil au rôle IAM créé et aux policies qui lui sont attachées:
tag de départ: 1.5-codepipeline-iam-role
tag d'arrivée: 1.6-codebuild-project
Dans cette partie, nous allons rajouter un projet CodeBuild
, qui sera déclenché par CodePipeline
.
Nous commençons par le projet CodeBuild
car un projet CodePipeline
doit avoir au minimum 2 "stages", qui seront dans notre cas : "Source", "Build".
Le stage "Source" a deja été anticipé par la création de la connexion Github
, maintenant on s'occupe d'anticiper le stage "Build".
Voici les mises à jour qui sont effectuées dans le template CloudFormation
:
Parameters:
[...]
GithubRepo:
Type: String
Description: Github source code repository
GithubRepoBranch:
Default: 'main'
Type: String
Description: Github source code branch
Resources:
[...]
PipelineRole:
Properties:
Policies:
PolicyDocument:
Statement:
[...]
- Effect: Allow
Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
- codebuild:BatchGetBuildBatches
- codebuild:StartBuildBatch
Resource: !GetAtt
- BuildProject
- Arn
[...]
BuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Join
- '-'
- - !Ref ApplicationName
- build-project
Description: A build project for !Ref ApplicationName
ServiceRole: !Ref BuildProjectRole
Artifacts:
Type: CODEPIPELINE
Packaging: ZIP
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
build:
commands:
- echo "hello world"
Nous rajoutons:
GithubRepo
: le nom du repo `GithubGithubRepoBranch
: la branche à récupérer, par défaut: main
CodeBuild
CodePipeline
de déclencher le projet CodeBuild
défini just aprèsOn aurait pu définir les paramètres et l'extension des permissions du projet CodePipeline
lors d'un futur commit, mais bon, ce qui est fait est fait, ça sera pour un futur article encore plus clean.
updatons la stack CloudFormation
via le script helper:
./create-all.sh my-app
On vérifie la bonne éxécution de la mise à jour de la stack, ainsi que la création de la nouvelle ressource:
On jette un oeil aux projets CodeBuild pour vérifier la création du projet:
On fait confiance à CloudFormation
pour avoir mis à jour les permission IAM sur le rôle dédié à CodePipeline
.
Pour le sport, on peut vérifier l'apparition des 2 nouvelles properties:
tag de départ: 1.6-codebuild-project
tag d'arrivée: 1.7-codepipeline-project
Dans cette partie, nous allons rajouter un projet CodePipeline
, qui sera composé de 2 stages:
Github
CodeBuild
par CodePipeline
Ce qui se traduit par l'ajout de la ressource CloudFormation
suivante:
Pipeline:
Description: Creating a deployment pipeline for !Ref ApplicationName project in AWS CodePipeline
Type: 'AWS::CodePipeline::Pipeline'
Properties:
RoleArn: !GetAtt
- PipelineRole
- Arn
ArtifactStore:
Type: S3
Location: !Ref S3Bucket
Stages:
- Name: Source
Actions:
- Name: Source
ActionTypeId:
Category: Source
Owner: AWS
Version: 1
Provider: CodeStarSourceConnection
OutputArtifacts:
- Name: SourceOutput
Configuration:
ConnectionArn: !Ref GithubConnection
FullRepositoryId: !Ref GithubRepo
BranchName: !Ref GithubRepoBranch
OutputArtifactFormat: "CODE_ZIP"
- Name: Build
Actions:
- Name: Build
InputArtifacts:
- Name: SourceOutput
OutputArtifacts:
- Name: BuildOutput
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
Configuration:
ProjectName:
Ref: BuildProject
Voir https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codepipeline-pipeline.html pour la documentation
Analysons un peu cette ressource:
CodePipeline
s'éxécutera avec le rôle définit dans l'étape #4CodeStarSourceConnection
. CodeStarSourceConnection
CodeStarSourceConnection
CodeBuild
. Dans la configuration, on référence le projet CodeBuild
créé dans l'étape #5updatons la stack CloudFormation
via le script helper:
./create-all.sh my-app
On vérifie la bonne éxécution de la mise à jour de la stack, ainsi que la création de la nouvelle ressource:
On vérifie la création et le statut de la pipeline dans "Developer Tools":
Si le statut est en échec, cela peut venir du fait que la connexion github n'est pas activée.
Nous l'avons fait lors de l'étape #2, mais si jamais vous avez par exemple, supprimé et recréé la stack CloudFormation
de la pipeline vous devrez activer à nouveau la connexion et relancer une release dans la pipeline:
La pipeline devrait alors s'être éxécutée avec succès:
Allons inspecter le résultat du build
Vérifions la console:
Nous avons bien pu logger "Hello World". Allons inspecter enfin le code source dans le bucket S3:
On note les répertoires créés par CodePipeline
et le lien avec les configurations "InputArtifacts" et "OutputArtifacts" du template CloudFormation
de notre pipeline.
Téléchargeons le zip et inspectons son contenu:
Le contenu du zip est bien le code source du projet. Félicitations, nous avons désormais une base réutilisable et assez générique, pour bootstrapper une pipeline de CI/CD.
tag de départ: 1.7-codepipeline-project
tag d'arrivée: 1.8-buildtime-test-junit-execution-local
Dans cette étape, nous créons un projet Java
minimal, avec juste le nécessaire pour éxécuter un test unitaire:
fichier pom.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>codebuild-test-report-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<junit-jupiter.version>5.7.2</junit-jupiter.version>
<assertj.version>3.19.0</assertj.version>
<maven-surefire-failsafe-plugin.version>3.0.0-M5</maven-surefire-failsafe-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-failsafe-plugin.version}</version>
</plugin>
</plugins>
</build>
</project>
Nous ajoutons:
surefire
, afin que les tests junit-5 soient lancés quand on éxécute: mvn test
Nous pouvons ainsi lancer des tests unitaires Junit
:
via l'IDE:
tag de départ: 1.8-buildtime-test-junit-execution-local
tag d'arrivée: 1.9-buildtime-test-junit-reporting-local
Dans cette étape, nous allons rajouter le reporting de couverture de test:
Pour cela nous effectuons les actions suivantes:
Maven
du nom de Jacoco
("JAva COde COverage")src/main/java
) avec une méthode quelconque, et couvrir cette méthode par un test. Sans classe de prod, le rapport de couverture est viderajouts dans le pom.xml
:
<properties>
[...]
<jacoco.version>0.8.7</jacoco.version>
</properties>
<build>
<plugins>
[...]
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>jacoco-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-coverage-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Analysons un peu la configuration de ce plugin:
prepare-agent
du plugin. Ce goal est par défaut bindé à la phase initialize
du lifecycle default
de Maven
report
du plugin. On redéfinit le binding de ce goal à la phase test
du lifecycle de Maven
car par défaut il est bindé à la phase verify
, et on voulait pouvoir avoir les rapport d'éxécution des tests unitaires lors d'un mvn test
. Même si concrètement, le binding par défaut est très bien, on n'a pas vraiment de cas où l'on souhaite les rapports de couverture de test sans avoir le .jar
, mais bon.Après avoir éxécuté mvn clean test
, les rapports de couverture sont disponibles dans target/site/jacoco/
, sous différents formats.
Jetons un oeil au .html
généré:
Voici un ensemble de références intéressantes pour Jacoco
, sur la configuration de plugins Maven
, ou encore sur les cycles de vie de ce dernier, et les différences entre cycle de vie, phase et goal:
tag de départ: 1.9-buildtime-test-junit-reporting-local
tag d'arrivée: 1.10-buildtime-test-junit-reporting-pipeline
Dans cette étape, nous réaliserons les actions suivantes:
CodeBuild
)CodeBuild
, destiné à mettre en cache le repository Maven
local, sinon à chaque déclenchement, il va retélécharger les dépendances depuis maven central ... et c'est longLes modifications dans les ressources du template CloudFormation
sont les suivantes:
BuildProject:
Type: AWS::CodeBuild::Project
Properties:
Cache:
Type: S3
Location: !Sub '${S3Bucket}/maven-cache'
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
install:
runtime-versions:
java: corretto11
build:
commands:
- mvn test
reports:
BuildTimeTests:
files:
- 'target/surefire-reports/TEST*.xml'
CoverageReport:
files:
- 'target/site/jacoco/jacoco.xml'
file-format: 'JACOCOXML'
cache:
paths:
- '/root/.m2/**/*'
Analysons les différences:
CodeBuild
, dans le répertoire maven-cache
du bucket S3 de la pipelineecho "hello world"
par mvn test
mvn verify
. Ces 2 types de tests sont éxécutés lors du build de l'application, aussi on les regroupe donc de cette manière. On cherche à les distinguer des tests éxécutés sur l'application après son déploiement sur un environnement, éxécutés habituellement après le build de l'applicationMaven
localMettons à jour la pipeline:
./create-all.sh my-app
Vérifions que le builspec est bien mis à jour:
Vérifions le cache:
Parfait. Lançons maintenant une release sur la pipeline et inspectons le résultat:
Voyons le détail du rapport d'éxécution:
Et le détail du rapport de couverture:
Parfait ! Je vous invite à retirer un test, ou encore rajouter une classe non testée, pour vérifier que le rapport est bien modifié.
mvn test
) en Cucumber dans la pipelinetag de départ: 1.10-buildtime-test-junit-reporting-pipeline
tag d'arrivée: 1.11-buildtime-unit-test-cucumber
Dans cette étape, nous allons créer un test unitaire en utilisant Cucumber
, mais éxécuté lors de la phase test̀
du cycle de vie de Maven.
Voici les actions que nous allons réaliser au cours de cette étape:
Cucumber est généralement utilisé pour des tests dits "d'intégration", pour une appli déployé sur un environnement, ou pour des tests end-to-end, là aussi sur un environnement.
Cependant, je trouve que Cucumber a largement sa place pour des tests purement unitaire, pour de la logique métier par exemple. Bien faits et bien entretenus, ils sont largement plus lisibles que des tests Junit purs, et peuvent être une bonne documentation / specs de la logique métier, et une spec toujours à jour, et fiable car éxécutable et éxécuté à chaque build. Je les déconseillerais p-e pour faire du TDD, après avoir essayé quelques fois, c'est beaucoup plus long que Junit pur, les boucles de rouge-vert-refacto sur trop longues et trop frustrantes.
Par contre, commencer en Junit pur,et substituer des tests unitaires ayant possiblement perdu en clareté après quelques jours ou semaines, par des tests Cucumber
bien écrits, m'a l'air d'une approche à creuser.
Ou alors p-e écrire ces tests Cucumber plus rapidement, soit à force d'en faire, soit avec une méthode ou des outils qui accélèreraient leur écriture (méthodes et outils que je ne connais pas à ce jour)
Nous ajoutons les dépendances Maven
suivantes (fichier pom.xml
):
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
La dépendance junit-vintage-engine
sert à éxécuter la classe "runner" de Cucumber, la librairie utilisée, io.cucumber
, s'appuyant sur junit-4 uniquement à l'heure de l'écriture de cette article, et ne fournit pas d'implémentation basée sur junit-5.
Nous ajoutons la classe à tester suivante (fichier src/test/resources/features/buildtime/test.feature
):
package org.example;
public class MyOtherClass {
public String hola(){
return "hola";
}
}
Nous ajoutons le test Cucumber
suivant (fichier src/test/resources/features/buildtime/test.feature
):
Feature: build time feature
Scenario: build time test scenario
When we call `MyOtherClass.hola` method
Then the response is "hola"
Nous ajoutons le runner Cucumber
suivant (fichier src/test/java/org/example/CucumberRunnerTest.java
):
package org.example;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;
@RunWith(Cucumber.class)
@CucumberOptions(
plugin = {
"pretty",
"junit:target/cucumber-reports/buildtime/cucumber-results.xml",
"usage:target/cucumber-reports/buildtime/cucumber-usage.json"},
glue = {"org.example"},
features = "src/test/resources/features/buildtime")
public class CucumberRunnerTest {
}
Nous ajoutons les steps d'implémentation Cucumber
suivantes (fichier src/test/java/org/example/CucumberStepDefinitions.java
):
package org.example;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.assertj.core.api.Assertions;
import static org.assertj.core.api.Assertions.assertThat;
public class CucumberStepDefinitions {
private String holaResponse;
@When("we call `MyOtherClass.hola` method")
public void whenWeCallOtherClassHola() {
holaResponse = new MyOtherClass().hola();
}
@Then("the response is {string}")
public void theResponseIs(String expectedResponse) {
assertThat(holaResponse).isEqualTo(expectedResponse);
}
}
On vérifie ensuite:
Depuis l'IDE, via le fichier de feature:
Depuis l'IDE, via la classe runner:
En ligne de commande:
Inspectons les rapports de couverture de test en local
Nous modifions le buildspec dans pipeline-cfn.yml
:
Analysons cette modification:
JUNITXML
, et avec une sortie plus propre que ce qui est généré par le plugin maven surefire
. On le rajoute donc à la liste des fichiers à prendre en compte pour générer le rapportVérifions les rapports dans générés dans la pipeline:
On note l'apparition du rapport de couverture de test
Inspectons le rapport de test dans la pipeline:
On note le résultat de l'éxécution du test
Cucumber
Inspectons le rapport de couverture de test:
On peut voir que la classe couverte par le test
Cucumber
vient de faire son apparition.
Parfait! Ici aussi je vous invite à supprimer un test, rajouter des classes non testés, des tests qui échouent, etc. pour jouer avec l'outil, observer la modification de la couverture de test dans le rapport
mvn verify
) en Junittag de départ: 1.11-buildtime-unit-test-cucumber
tag d'arrivée: 1.12-buildtime-integation-test-junit
Dans cette étape, nous allons rajouter des tests dits d'intégration, mais éxécutés lors du build de l'application.
Typiquement des tests vérifiant que l'appli marche avec une base de données, une file de message, etc. embarqués, in-memory, ou dockerisés.
J'apprécie en particulier TestContainers
pour ce genre de tests (bien que je n'ai pas encore réussi à le faire tourner dans CodeBuild
-> pour un prochain article si j'y arrive)
Nous voulons séparer ce genre de test, plus longs que les tests unitaires (entre quelques secondes, jusque parfois quelques minutes), car, au moins en TDD, on lance les tests continuellement (on a même des outils à disposition pour les lancer littéralement en continu), et attendre 20 secondes à 2 minutes, c'est pas possible.
Ainsi on souhaite séparer les tests unitaires testant des fonctionnalités "coeur", très rapides, des tests "d'intégration", plus longs. Mais qu'ils soient tous lancés lors de la phase de build, dans la pipeline.
On utilise le plugin failsafe
de Maven
pour cela, et éxécuter ces tests lors de la phase integration-test
.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-surefire-failsafe-plugin.version}</version>
<executions>
<execution>
<id>build-time-integration-test</id>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
</executions>
</plugin>
Le plugin failsafe
n'est par défaut lié à aucune phase du cycle de vie.
Cependant le goal integration-test
est par défaut lié au cycle de vie ... integration-test
.
Nous rajoutons aussi une méthode à la classe MyClass
:
public String integrationHello(){
return "integrationHello";
}
Et une classe de test d'intégration MyClassIT
pour tester cette méthode:
package org.example;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class MyClassIT {
@Test
void test() {
MyClass myClass = new MyClass();
assertThat(myClass.integrationHello()).isEqualTo("integrationHello");
}
}
On vérifie que ces nouveaux tests ne se lancent pas lors d'un mvn test
, mais se lancent lors d'un mvn verify
mvn test
:
Et pas de trace de
target/failsafe-report
, ni de MyClassIT
dans target/surefire-report
:
mvn verify
:
On peut voir dans la console que le test d'intégration est bien éxécuté.
Et cette fois-ci, le dossier
target/failsafe-report
est apparu, et contient le rapport de test de MyClassIT
Cependant, le rapport de couverture de test ne prend pas encore en compte la couverture des tests d'intégration:
Nous modifions la configuration du plugin Jacoco
de manière à avoir le comportement suivant:
target/coverage-reports/jacoco-ut.exec
target/site/jacoco-ut
target/coverage-reports/jacoco-it.exec
target/site/jacoco-it
target/coverage-reports/aggregate.exec
2) génération du rapport de couverture aggrégé dans `target/site/jacoco-aggregateRelançons un coup de mvn clean verify
et vérifions le rapport de couverture aggrégé:
Cette fois-ci, MyClass
est couverte à 100%.
On ne montrera pas de capture d'écran, vous pouvez le vérifier par vous-mêmes, dans les résultats partiels, jacoco-ut
et jacoco-it
, on observe une couverture partielle.
Pour cela rien de plus simple, dans le
buildspec
du template de la pipeline:
mvn verify
au lieu de mvn test
failsafe
target/site/jacoco-aggregate/jacoco.xml
, au lieu de target/site/jacoco/jacoco.xml
Poussons le code et vérifions les rapports de test générés:
./create-all.sh my-app
mvn verify
) en Cucumbertag de départ: 1.12-buildtime-integation-test-junit
tag d'arrivée: 1.13-buildtime-integation-test-cucumber
Dans cette étape, nous rajoutons un test d'intégration "build time", mais en cucumber cette fois-ci.
Pour réaliser cela, nous allons:
src/main/java/org/example/MyClassCucumberIT
src/test/resources/features/buildtime/test-integration.feature
mvn verify
: src/test/java/org/example/CucumberRunnerIT
src/test/java/org/example/CucumberStepDefinitionsIT
CloudFormation
de la pipeline afin de:
failsafe
et par Cucumber)Voici la nouvelle classe de prod:
package org.example;
public class MyClassCucumberIT {
public String helloCucumberIT(){
return "helloCucumberIT";
}
}
Voici le scenario des tests d'intégration Cucumber
:
@integrationTest
Feature: build time integration feature
Scenario: build time integration test scenario
When we call `MyClassCucumberIT.helloCucumberIT` method
Then the `MyClassCucumberIT.helloCucumberIT` response is "helloCucumberIT"
La modification du scenario des tests unitaires Cucumber
:
@integrationTest
Feature: build time integration feature
Scenario: build time integration test scenario
When we call `MyClassCucumberIT.helloCucumberIT` method
Then the `MyClassCucumberIT.helloCucumberIT` response is "helloCucumberIT"
Voici le runner Java pour les tests d'intégration:
package org.example;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;
@RunWith(Cucumber.class)
@CucumberOptions(
plugin = {
"pretty",
"junit:target/cucumber-reports/buildtime/cucumber-integration-results.xml",
"usage:target/cucumber-reports/buildtime/cucumber-integration-usage.json"},
glue = {"org.example"},
tags = "@integrationTest",
features = "src/test/resources/features/buildtime")
public class CucumberRunnerIT {
}
le runner java modifié pour les tests unitaires:
@RunWith(Cucumber.class)
@CucumberOptions(
plugin = {
"pretty",
"junit:target/cucumber-reports/buildtime/cucumber-results.xml",
"usage:target/cucumber-reports/buildtime/cucumber-usage.json"},
glue = {"org.example"},
tags = "@unitTest",
features = "src/test/resources/features/buildtime")
public class CucumberRunnerTest {
}
La classe d'implémentation des steps des tests d'intégration:
package org.example;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import static org.assertj.core.api.Assertions.assertThat;
public class CucumberStepDefinitionsIT {
private String holaResponse;
@When("we call `MyClassCucumberIT.helloCucumberIT` method")
public void whenWeCallOtherClassHola() {
holaResponse = new MyClassCucumberIT().helloCucumberIT();
}
@Then("the `MyClassCucumberIT.helloCucumberIT` response is {string}")
public void theResponseIs(String expectedResponse) {
assertThat(holaResponse).isEqualTo(expectedResponse);
}
}
la modification du buildspec:
Mettons à jour la pipeline:
./create-all.sh my-app
Redéclenchons la pipeline et inspectons les rapports de test:
éxécution des tests:
Très bien. Après tous ces efforts, nous avons une pipeline qui :
Prochaine étape dans cette aventure: exposer un endpoint REST et déployer notre application Java dans une instance EC2, puis dans un auto-scaling group derrière un Load Balancer.
À bientôt.