• 10 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 9/20/24

Couvrez les besoins utilisateurs avec les tests d’intégration

Vous avez abordé jusqu'à présent le TDD comme une bonne pratique pour créer des tests unitaires principalement. L'objectif ici est de découvrir une adaptation de cette méthode pour traduire les exigences métiers sous forme de tests d'acceptation puis de tests d'intégration. Pour cela, nous allons découvrir le TDD de Londres.

Le TDD de Londres : un nouveau type de TDD

Au milieu des années 2000, la communauté eXtreme Programming londonienne a commencé à s’inquiéter du manque de prise en considération des besoins de nos utilisateurs. Elle a donc inventé un style de TDD qui aidait à garantir que nos tests décrivent réellement les problèmes de nos utilisateurs.

Avec AssertJ, nous avions déjà tenté de rendre plus lisibles les assertions. Mais là, il s'agit de revoir complètement l'écriture du test !

En quoi est-ce différent du TDD que nous avons vu jusqu'à présent ?

Le TDD que nous avons vu se concentre surtout sur les tests au niveau des fonctionnalités unitaires. C’est au développeur de s’assurer que ces tests reflètent les exigences du client. Ce type de test va de l’intérieur vers l’extérieur, des plus petites unités de code jusqu’à ce avec quoi les utilisateurs interagissent.

Par opposition, le TDD de Londres commence par les prérequis métiers. Au lieu de se concentrer sur ce qui est fait dans le code, comme l’ajout d’un utilisateur dans la base de données, le TDD de Londres se concentre sur ce dont l’utilisateur a besoin de la part de votre programme. Le fait de formuler les exigences comme « un utilisateur doit pouvoir s’inscrire » décrit le comportement d’un programme à travers les besoins utilisateur. Autrement dit, vous travaillez avec des tests d’acceptation ! C’est pourquoi on la désigne aussi sous le nom de développement piloté par les tests d’acceptation (ATDD pour « acceptance test-driven development », en anglais), ou test de l’extérieur vers l’intérieur.

Ainsi, vous allez développer uniquement des classes qui doivent faire passer vos tests d’acceptation. Cela peut vous aider à développer moins de code inutile.

Comment est-ce que je commence de l’extérieur pour travailler vers l’intérieur ?

Comme je l'ai mentionné, votre test d’acceptation est habituellement placé de façon optimale dans vos tests d’intégration ou au-dessus, car il décrit une exigence business. De l’extérieur vers l’intérieur signifie que vous commencez par le test d’acceptation puis descendez la pyramide en développant les tests nécessaires.

Commencez par vous poser les questions suivantes :

  1. Quel est le test d’acceptation ?

  2. Avez-vous des tests unitaires qui couvrent ce dont vous avez besoin pour le test d’intégration ?

  3. Avez-vous un test d’intégration qui couvre ce dont vous avez besoin pour le test de bout en bout ?

  4. Avez-vous un test de bout en bout qui représente ce test d’acceptation ?

À chaque non, vous écrivez un test et demandez ce dont vous avez besoin pour réussir ce test. La réponse : davantage de tests plus bas dans la pyramide ! La plupart du temps, aucun test ne couvre vraiment le test d'acceptation. Donc, il s'agit de partir de l'extérieur (le besoin métier) vers l'intérieur (l'unité de code testée). La démarche est illustrée par la figure ci-dessous :

Démarche de tests de l'extérieur vers l'intérieur
Démarche de tests de l'extérieur vers l'intérieur

En écrivant chaque test, vous creusez un chemin de l’extérieur de votre code vers l’intérieur, en construisant uniquement les parties dont vous avez besoin. Écrivez des tests jusqu’à être sûr d’avoir couvert tous les besoins de votre test d’acceptation.

Ensuite, vous commencez à écrire le code, en remontant à partir du bas de la pyramide, pour retourner jusqu’au sommet. Quand vous construisez ce code fonctionnel, vos tests unitaires, d’intégration et si nécessaire de bout en bout commencent tous à réussir. Il y aura toujours quelques ajustements à faire, mais vous écrirez moins de lignes de code, qui seront également plus ciblées. Vous écrirez toujours de nombreux tests unitaires, mais ce seront les seuls dont vous aurez besoin.

Appliquez le TDD de Londres

Le TDD de Londres vous encourage également à travailler en cycles courts, un test à la fois :

Séquence du TDD de Londres
Séquence des actions pour effectuer du TDD de Londres

Ce schéma est un peu dense, décomposons-le !

  • Dans les étapes 1 et 2, vous prenez votre test d’acceptation et vous écrivez des cas de test pour lui. Il s’agit de faire échouer les tests d’intégration (et, si nécessaire seulement, des tests fonctionnels de bout en bout) ! 🛑

  • Les étapes 3 à 5 indiquent que vous appliqueriez le TDD à une unité de code, ce qui aide à construire le code réel pour satisfaire au test d’acceptation. ✅ 

  • Les étapes 6 et 7 sont la boucle de refactoring dont nous avons parlé dans les chapitres précédents, où vous nettoyez votre code sans casser les tests. 🌀

  • À partir de l’étape 8 :

    • si vos tests d’acceptation échouent encore, il vous faut continuer à écrire davantage de code et retourner à l’étape 3,

    • si vous en avez suffisamment pour que cela ait commencé à réussir, allez à l’étape 8 et remontez le cycle pour commencer un nouveau test d’acceptation ! 💫

C’est encore assez abstrait, nous allons donc construire une application web de calculateur ensemble en utilisant ces cycles. Une fois le développement terminé, il doit pouvoir additionner, soustraire, diviser, et multiplier.

Oh la la, ça fait beaucoup ! Par où je commence ?

Si vous commencez par l’addition, vous aurez un point de départ gérable. Par exemple, vous pouvez créer un test d’acceptation qui dit : « un utilisateur doit pouvoir additionner deux nombres et voir leur somme ». Souvenez-vous, un test à la fois !

Quels tests vous attendez-vous à obtenir ? Parcourons les étapes ensemble :

  • étape 1 : commencez par écrire un test de bout en bout rouge.
    Ce test ne doit pas se préoccuper de l’aspect de la page web, mais peut automatiser la visite d’une page web, la sélection de deux nombres, et le clic sur un bouton « = ». Par exemple : « un utilisateur doit pouvoir additionner deux nombres et voir leur somme » ;

  • étape 2 : exécutez le test rouge ci-dessous, et il échouera car vous n’avez pas construit votre serveur web :

  1. TYPE DE TEST

  • NOM DU TEST

  • CE QUI EST TESTÉ

  • Test d’acceptation

  • Un utilisateur doit pouvoir additionner deux nombres et voir leur somme

  • Un utilisateur visite une page, entre deux nombres, clique sur « = » et voit un résultat.

  • étape 3 : écrivez un test d’intégration pour un serveur web auquel vous pouvez envoyer deux nombres :

  1. TYPE DE TEST

  • NOM DU TEST

  • CE QUI EST TESTÉ

  • Intégration

  • GivenTwoNumbers

  • WhenAdded

  • ThenTheyShouldBeSummed

  • Le serveur peut démarrer, et a un contrôleur qui acceptera deux nombres, et les donnera à une classe de calculateur pour les additionner.

  • étape 4 : choisissez votre framework favori (comme Spring Boot) pour construire un contrôleur auquel vous pouvez envoyer deux nombres ;

  • étape 5 : exécutez vos tests de bout en bout et d’intégration rouge : le serveur ne fait toujours rien de vos nombres ;

  • étape 6 : maintenant, écrivez un cas de test unitaire rouge pour de nouvelles classe et méthode : Calculator.add(Integer a, Integer b) ;

  • étape 7 : créez Calculator.add, et vos tests sont rouges :

  1. TYPE DE TEST

  • NOM DU TEST

  • CE QUI EST TESTÉ

  • UNITAIRE

  • add_Sums_PositiveAndPositive

  • Calculator.add(1, 1)

  • UNITAIRE

  • add_Sums_NegativeAndPositive

  • Calculator.add(-1, 1)

  • UNITAIRE

  • add_Sums_NegativeAndNegative

  • Calculator.add(-1, -1)

  • UNITAIRE

  • add_Sums_PositiveIntegerAndZero

  • Calculator.add(1, 0)

  • UNITAIRE

  • add_Sums_ZeroAndZero

  • Calculator.add(0, 0)

  • étape 8 : faites fonctionner Calculator.add et tous vos tests passent au vert ;

  • étape 9 : vous pouvez maintenant nettoyer (refactorer) votre code et vous assurer que le test reste vert ;

  • étape 10 : passez à l’écriture d'autres cas de test unitaire pour la même fonctionnalité, en prenant en compte les scénarios alternatifs/limites ;

    étape 11 : et maintenant, écrivez un autre test d’intégration, et répétez les étapes 3 à 10 !

INTÉGRATION

GivenBadValues _WhenAdded

ThenAnErrorIsReturned

Le serveur démarre ; le contrôleur valide l’input et fournit une erreur.

UNITAIRE

add_ThrowsException_AddingToNull

Calculator.add(null, 1)

UNITAIRE

add_ThrowsException_AddingNull

Calculator.add(1, null) 

Découvrez le développement piloté par le comportement (BDD)

Quelques définitions

Super ! Vous avez la méthodologie pour vous concentrer sur les besoins métiers, mais comment réussir à parler le même langage avec le client et les utilisateurs, et plus généralement des non-informaticiens ? Pour une meilleure collaboration entre intervenants d'un projet, réussir à construire une compréhension partagée des cas de test, grâce au langage courant, est une des clés de la réussite.

Cette approche va de pair avec les pratiques agiles : réunissez-vous et définissez ces besoins dans le langage qu’utilise votre client. Vous pourriez proposer ce qui suit au tableau blanc :

LA FONCTIONNALITÉ QUE NOUS DÉVELOPPONS : Additionner deux nombres

QUE VEUT L’ÉTUDIANT ?

En tant qu’étudiant, je veux additionner deux nombres pour pouvoir résoudre des calculs compliqués.

PAR EXEMPLE :

En supposant qu’un élève utilise le Calculateur quand 2 et 5 sont additionnés, on devrait montrer 7 à l’élève.

La phrase commençant par "En tant que..." est ce qu'on appelle, en agilité, un récit utilisateur. Il décrit une fonctionnalité en n'oubliant pas qui est la cible ni la finalité de la fonctionnalité. En donnant un exemple, on fabrique naturellement un test d'acceptation en langage naturel.

Oh la la, ça fait beaucoup de travail manuel, non ?

Non, pas tant que ça ! Vous pouvez utiliser un outil populaire nommé Cucumber pour automatiser le développement dans le style BDD. Il vous aide à décrire vos scénarios en langage naturel, et surtout à faire le lien avec vos tests fonctionnels ou d'intégration.

Comment fonctionne Cucumber ?

Cucumber décrit une fonctionnalité que vous allez développer en utilisant un fichier de fonctionnalité. C’est l’équivalent d’une partie de votre application que vous allez construire de façon incrémentale. Par exemple, « gérer l’addition » pourrait être une fonctionnalité de votre calculateur.

Gherkin est le nom donné au langage que Cucumber utilise pour les tests. Il s’agit d’une structure pour décrire le comportement attendu de votre logiciel dans différentes situations. Le langage Gherkin décrit un scénario de test avec une phrase de structure suivante :

  • Given some preconditions / Étant donné les conditions préalables

  • When some action / Quand une action

  • Then what expectations to assert / Alors les attentes à affirmer

Voici un exemple de fichier de fonctionnalité pour le calculateur, avec un choix de langage en français :

# language: fr

Fonctionnalité:  Additionner deux nombres
En tant qu'élève, je veux additionner deux nombres pour pouvoir résoudre des calculs compliqués.

Scénario: Additionner deux nombres positifs
Étant donné un élève utilise le Calculateur
Quand 2 et 5 sont additionnés
Alors on montre 7 à l'élève
  • ligne 1 : on spécifie la langue ;

  • lignes 3-4 :  on explicite une fonctionnalité sous forme de titre (ligne 3) et de description en dessous (ligne 4) ;

  • lignes 6-9 : on détaille un scénario avec un titre (ligne 6), l'étape Given (ligne 7), l'étape When (ligne 8) et l'étape Then (ligne 9).

Écrivez un test d’acceptation Cucumber

Tout d’abord, voyons comment ajouter Cucumber à votre projet et le mettre en place pour les tests avec Spring, puis créons un premier test d'intégration en lien avec le test d'acceptation issu du fichier texte.

Ajout de Cucumber dans la configuration Maven

Modifiez le fichier pom.xml pour y intégrer Cucumber. On définit une property pour indiquer la version de Cucumber (rendez-vous sur mvnrepository.com !), et ajouter les dépendances nécessaires.

Vous devrez aussi ajouter une dépendance spéciale de JUnit : junit-vintage-engine. Ce composant permet de lancer des tests JUnit 4 dans un système JUnit 5. À ce jour, Cucumber n'est pas compatible avec JUnit 5.

Voici les modifications à faire dans le fichier pom.xml :

<properties>
        ...
		<dep.cucumber.version>4.8.0</dep.cucumber.version>
</properties>
<dependencies>
    ...
    		<dependency>
			<groupId>org.junit.vintage</groupId>
			<artifactId>junit-vintage-engine</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-jvm</artifactId>
            <version>${dep.cucumber.version}</version>
            <scope>test</scope>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-spring</artifactId>
            <version>${dep.cucumber.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>${dep.cucumber.version}</version>
            <scope>test</scope>
        </dependency>
</dependencies>
Ajout du fichier de fonctionnalité

Ensuite, copiez-collez le fichier de fonctionnalité rappelé en exemple ci-dessous, dans un nouveau fichier  src/test/resources/features/calcul-addition.feature  :

# language: fr

Fonctionnalité:  Additionner deux nombres
En tant qu'élève, je veux additionner deux nombres pour pouvoir résoudre des calculs compliqués.

Scénario: Additionner deux nombres positifs
Étant donné un élève utilise le Calculateur
Quand 2 et 5 sont additionnés
Alors on montre 7 à l'élève
Création du test d'acceptation Cucumber

Vous allez ensuite créer le lanceur de tests d'acceptation Cucumber. En fait, ce lanceur va être reconnu comme un test JUnit, mais il peut y avoir autant de tests que de fonctionnalités définies dans les fichiers texte.

Pour qu'il soit exécutable par Maven en tant que test d'intégration, vous pouvez créer une classe  CucumberAIT  (pour Acceptance Integration Test), dans un nouveau paquetage  org.openclassrooms.testing.cacul.acceptance

Cette classe ne contient pas directement les tests mais simplement des indications au lanceur, comme, par exemple, le chemin vers les fichiers de fonctionnalité :

@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources/features", plugin = { "pretty", "html:target/html-cucumber-report" })
public class CucumberAIT {
}
Création des étapes Given/When/Then sous forme de test d'intégration système

Les étapes Given/When/Then vont être implémentées comme des méthodes d'une classe de test d'intégration. Donc, au lieu d'avoir une méthode de test intégrant les 3 étapes :

@Test
public void givenA_whenB_thenC() {
	// GIVEN
	...

	// WHEN
	...

	// THEN
	...
}

vous allez implémenter :

@Given("A")
public void givenA() {
    ...
}

@When("B")
public void whenB() {
	...
}

@Then("C")
public void thenC() {
    ...
}

Les annotations @Given, @When et @Then viennent de Cucumber. Le contenu textuel des annotations doit alors correspondre au contenu textuel des fichiers de fonctionnalités ! Et là où c'est intéressant, c'est que vous pouvez paramétrer les contenus textuels dans vos méthodes !

Je m'explique : votre fichier de fonctionnalité donne des scénarios d'exemples. Ici : tester 2 + 5 = 7. Vous pouvez récupérer ces arguments d'exemple en arguments de méthode. Voilà ce que ça donne pour le fichier de fonctionnalité d'addition :

@Given("un élève utilise le Calculateur")
public void a_student_is_using_the_Calculator() {
	...
}

@When("{int} et {int} sont additionnés")
public void and_are_added(Integer leftArgument, Integer rightArgument) {
	...
}

@Then("on montre {int} à l'élève")
public void the_student_is_shown(Integer expectedResult) {
	...
}

Le contenu textuel est paramétré par {int} et les méthodes concernées :

  • à la méthode annotée @When, on récupère les deux arguments de l'addition (2 et 5) ;

  • à la méthode annotée @Then, on récupère l'argument de résultat attendu (7).

Cela signifie que l'on pourrait rédiger d'autres exemples d'additions dans les fichiers de fonctionnalités, et réutiliser les mêmes méthodes d'étapes !

Il ne nous reste plus qu'à implémenter ces étapes. Nous allons le faire dans une nouvelle classe CalculatorSteps. Vos fichiers sources devraient alors être structurés ainsi dans Eclipse :

Capture d'écran de Eclipse
Structure des fichiers pour l'exemple Cucumber

Nous allons réaliser un test d'intégration système basé sur Spring Boot, comme on l'a vu au chapitre précédent, mais sans mocks. en effet, Cucumber n'est pas compatible avec @MockBean.

On utilise alors deux annotations pour configurer Spring Boot avec tous les services : @SpringBootTest et @AutoConfigureMockMvc. Nous ne rentrerons pas dans les détails de ces annotations. Ensuite, nous implémentons les étapes Given/When/Then d'une façon similaire à CalculatorControllerSIT du chapitre précédent, mais avec un scénario encore plus proche de l'utilisateur :

  • au Given, on crée une requête GET pour vérifier que le formulaire est disponible à l'utilisateur ;

  • au When, on initialise les variables en fonction du contenu textuel du fichier de fonctionnalité ;

  • au Then, on soumet le formulaire POST et on vérifie la réponse comme au chapitre précédent.

Je vous laisse prendre le temps de regarder le code complet de la classe CalculatorSteps :

@SpringBootTest
@AutoConfigureMockMvc
public class CalculatorSteps {

	@Inject
	MockMvc mockMvc;

	private Integer lastLeftArgument;
	private Integer lastRightArgument;
	private String calculationType;

	@Given("un élève utilise le Calculateur")
	public void a_student_is_using_the_Calculator() throws Exception {
		mockMvc.perform(MockMvcRequestBuilders.get("/calculator"))
				.andExpect(MockMvcResultMatchers.status().is2xxSuccessful());
	}

	@When("{int} et {int} sont additionnés")
	public void and_are_added(Integer leftArgument, Integer rightArgument) throws Exception {
		lastLeftArgument = leftArgument;
		lastRightArgument = rightArgument;
		calculationType = "ADDITION";
	}

	@Then("on montre {int} à l'élève")
	public void the_student_is_shown(Integer expectedResult) throws Exception {
		final MvcResult result = mockMvc
				.perform(MockMvcRequestBuilders.post("/calculator").param("leftArgument", lastLeftArgument.toString())
						.param("rightArgument", lastRightArgument.toString()).param("calculationType", calculationType))
				.andExpect(MockMvcResultMatchers.status().is2xxSuccessful())
				.andReturn();

		assertThat(result.getResponse().getContentAsString()).contains(">" + expectedResult + "<");
	}
}

En résumé

  • Le développement piloté par le comportement (ou BDD) implique de collaborer avec votre équipe, les product owners, et d’autres décideurs, pour écrire des tests d’acceptation business en utilisant un langage métier. Ils deviennent vos premiers tests qui échouent.

  • Le TDD de Londres ou test de l’extérieur vers l’intérieur implique de prendre ces tests d’acceptation et d’appliquer le rouge-vert-refactor des tests d’intégration système vers l’intérieur, vers les tests unitaires et les classes nécessaires à les satisfaire.

  • Cucumber utilise un fichier de fonctionnalité non technique contenant des scénarios de tests d’acceptation. Ces scénarios testent automatiquement en écrivant des définitions d’étape pour correspondre au code. 

Si l'on a pu comprendre les tests d'acceptation et implémenter un exemple avec un test d'intégration système, il reste encore un bout d'ascension à faire sur la pyramide des tests, pour atteindre les tests fonctionnels de bout en bout !

Example of certificate of achievement
Example of certificate of achievement