Qualité logicielle et tests

 

Master Expert Technologie de l'information EPITECH 2020.

Co-fondateur et CTO d'une startup dans l'Edtech 2019 - fin 2022. (+3 ans)

Formation PSPO-1 Agile Scrum 2022.

Co-fondateur et CTO d'une startup dans la Deeptech fin 2022 - aujourd'hui.

Valentin MONTAGNE

1

Enjeux des tests automatisés et tests unitaires

3

Test Driven Development

5

Soutenance de fin de module

2

Tests end-to-end

4

Behavior Driven Development

Déroulement du cours

Chaque séance débutera par la présentation d'un concept et de l'intérêt d'utilisation de celui-ci.

1

Théorie

Après la théorie, nous verrons alors la pratique en réalisant des exercices.

2

Pratique

Nous verrons ensemble la correction des travaux pratiques. N'hésitez pas à poser vos questions.

3

Correction

Déroulement des journées

Soutenance

QCM et projet

Rendu TP et exercices

Pour faciliter le rendu final des TPs et exercices, vous allez créer un dépôt sur Github avec le nom Qualité&Tests en publique et le cloner sur votre machine.

 

Si vous préférez le garder en privé, vous devez m'ajouter avec les droits pour voir votre dépôt avec le nom d'utilisateur : ValentinMontagne

Connaissez-vous les tests automatisés ?

Enjeux des tests automatisés

1.

Pourquoi tester et vérifier la qualité ?

Protéger les entreprises d'incidents techniques

Créer un lien de confiance et garder les utilisateurs 

Augmenter la productivité et le bien-être des équipes techniques

Pourquoi automatiser les tests ?

Réduire le temps et le coût des tests

Éviter les erreurs humaines des tests manuels

Éviter les régressions à la source

Le principe de non-regression

La régression est un type de bug, une fonctionnalité déjà présente et fonctionnel dans la solution n'est maintenant plus utilisable.

 

Un autre type de régression que la régression fonctionnelle existe, on l'appelle la régression de performance. Plus précisément, la fonctionnalité consomme maintenant bien plus de ressources pour fonctionner.

 

C'est un principe fondamental du développement d'éviter les bugs et les régressions.

Par exemple en Agilité, chaque itération du produit doit développer le produit, jamais diminuer involontairement sa qualité ou ses fonctionnalités.

Pyramide des tests

Les différents types de tests

Unitaires - Teste la logique d'une fonction isolée de la solution qui ne peut plus être divisé en une plus petite unité.

 

Intégrations - Teste la logique d'une fonction liée à l'infrastructure comme une base de donnée ou un service externe.

 

De bout en bout - Teste une fonctionnalité comme un utilisateur final.

 

De fumé - Teste les éléments critiques de la solution pour éviter des répercussions trop importantes sur l'entreprise.

Les méthodologies pour mieux tester

Domain Driven Design

Behavior Driven Development

Test Driven Development

Tester facilement avec une bonne conception technique

Domain Driven Design

Isoler les couches logiques

La couche domaine (logique métier) met en œuvre la logique commerciale centrale, indépendante des cas d'utilisation, du domaine/système.
La couche application met en œuvre les cas d'utilisation de l'application basés sur le domaine. Un cas d'utilisation peut être considéré comme une interaction de l'utilisateur avec l'interface utilisateur (UI).
La couche présentation contient les éléments de l'interface utilisateur (pages, composants) de l'application.
La couche infrastructure soutient les autres couches en mettant en œuvre les abstractions et les intégrations avec les bibliothèques et les systèmes tiers.

Tester facilement avec une bonne conception technique

L'automatisation des tests

Gitlab CI

Github Actions

Cypress

Playwright

Cela permet aux développeurs de gérer les régressions à chaque commit et de pouvoir avoir des rapports d'erreurs complets.

Pourquoi ne pas tout tester ?

Votre temps est limité

Certains tests ne sont pas utiles

Perte d'intérêt si les tests durent trop longtemps à chaque itération

Des questions ?

Les tests unitaires

2.

Définition

Le test unitaire (ou « T.U. », ou « U.T. » en anglais) est une procédure permettant de vérifier le bon fonctionnement d'une partie précise d'un logiciel ou d'une portion d'un programme (appelée « unité » ou « module »).

Tester la plus petite partie non divisible d’un code source

Le but est de s'assurer que chaque partie de la solution fonctionne comme prévue. C'est pour cela que l'on se focalise sur le fait de tester indépendamment chaque partie.

 

Pour vérifier que l'ensemble de le solution soit testée, on parle alors de "Coverage" qui indique à quel pourcentage la solution est couverte par des tests unitaires.

 

Chaque test unitaire doit être indépendant des autres tests, on doit pouvoir choisir précisément de ne lancer qu'un des tests d'une partie de la solution.

Définir les valeurs limites et cas extrêmes à tester

En Behavior Driven Design, on utilise l'atelier de l'Example Mapping pour permettre à 3 personnes représentant chacune le produit, l'utilisateur et la technique pour définir toutes les règles d'une User Story.

 

Dans d'autres méthodologies, l'exercice est souvent aussi recommandé à plusieurs pour avoir des points de vues différents.

 

Je recommande l'utilisation d'User Flow pour être capable de comprendre dans quelles conditions et comment l'utilisateur réalise des actions sur la solution.

Qu'est-ce qu'une assertion ?

Une assertion permet de vérifier un résultat connu à l'avance.

# Everything is fine.
assert(3+3, 6);

# Will trigger an error
assert(3+3, 5);

# Using multipleBy2 example function
assert(multipleBy2(2), 4);

# 'expect' tool is used to create assertion with a better readability
expect(multipleBy2(2)).toBe(4);
# 'expect' has a lot of features (https://vitest.dev/api/expect.html)
expect(multipleBy2(2)).toBeTypeOf('number');

Les tests doivent être le plus lisible possible.

En Behavior Driven Development, des personnes non-techniques doivent être capable de lire les tests.

Qu'est-ce qu'un Mock ?

Un mock permet de simuler un objet ou une fonction dans votre solution. Cela vous permet de ne pas dépendre de votre vraie base de donnée ou de votre vraie service de paiement pour tester votre code.

import { vi } from 'vitest'

# Functions
const fn = vi.fn();

fn('hello world');
fn.mock.calls[0] === ['hello world'];

# Objects
const market = {
  getApples: () => 100
};
const getApplesSpy = vi.spyOn(market, 'getApples');

market.getApples();

getApplesSpy.mock.calls.length === 1

Jest vs Vitest

Jest est le framework historique pour rédiger les tests en Javascript. Ces avantages : sa simplicité, sa compatibilité et sa documentation.

Vitest est tout récent, il est couplé à Vite qui est un outil de gestion de projet Javascript. Ces avantages : sa rapidité et sa simplicité si l'on utilise déjà Vite.

 

Voyons ensemble le benchmark entre Jest et Vitest.

Pour la pratique, nous choisirons Vitest.

TP - Réaliser les tests unitaires d'une solution bancaire

Présentation de la librairie

Le but de cette librairie est de réaliser la logique métier d'une API bancaire. Elle a été créée avec Vite et utilise Vitest pour ces tests unitaires.

La librairie suit les bonnes pratiques en terme de conception, chaque entité est dans son domaine d'activité qui est dans le dossier "modules".

Les fichiers ".repository.js" gèrent le stockage des données en base de donnée. Les fichiers ".service.js" gèrent la logique métier.

La librairie utilise une base de donnée Postgres pour fonctionner.

Voici un exemple de configuration avec Docker pour lancer la base de donnée :

docker run --name postgres -p 5432:5432 \
        -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb \
        -v ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql -d postgres

Création de notre premier test

Pour lancer nos tests nous utiliserons la commande :

npm run test

Cette commande permet de "watch" les fichiers et de relancer les tests à chaque changement.

 

Pas d'inquiétudes si les tests du domaine "banking" ne fonctionne pas, ils sont simplement vides.

Création de notre premier test

La logique est déjà réalisé pour l'entité "User" dans le domaine "Authentication", nous allons donc débuter notre premier test pour vérifier que la création d'User marche correctement.

import { describe, it } from "vitest";

describe("User Service", () => {
  it("Create user", async () => {});
});

Nous allons tester la fonction "createUser" du service. Nous réalisons un test unitaire. Un test unitaire est isolé donc nous allons éviter la connexion à la base de donnée Postgres.

Pour cela, nous allons devoir Mock la fonction "createUserInRepository" pour éviter à notre test d'appeler la requête SQL.

Création d'un Mock - 1

Pour Mock une fonction, nous devons l'importer et créer un Mock du module dont elle est importé. En principe, un module possède plusieurs éléments, et l'on cherche souvent à ne Mock qu'une partie du module, c'est pour cela qu'ici nous importerons l'original et l'ajouterons au module Mock.

Ici "await" permet d'attendre que le module soit bien chargé et '...' indique à Javascript de fusionner l'objet en retour de "importOriginal" à l'objet que nous somme entrain de générer.

import {...}

vi.mock("./user.repository", async (importOriginal) => ({
  ...(await importOriginal()),
  createUserInRepository: vi.fn(),
}));

describe("User Service", () => {
	...

Création d'un Mock - 2

Maintenant que nous avons Mock la fonction "createUserInRepository", nous allons ajouter de la logique pour simuler le fait que l'objet a été écrit en base de donnée et renvoyé.

import {...}

vi.mock("./user.repository", async (importOriginal) => ({
  ...(await importOriginal()),
  createUserInRepository: vi.fn((data) => {
    return {
      id: 4,
      name: data.name,
      birthday: data.birthday,
    };
  }),
}));

describe("User Service", () => {
	...

Création d'un Mock - 3

Le plus dur est fait !

Il ne reste plus qu'à appeler la fonction que nous testons avec quelques arguments. Créons le test dans le premier "it" qui sert à vérifier la création d'un User. Ici "describe" permet de décrire dans quel contexte sont fait les tests. "It" doit être utilisé pour exprimer le résultat de chaque test, comme par exemple :
It("should trigger error on user creation for too young user")

...

describe("User Service", () => {
  it("should create an user", async () => {
    const user = await createUser({
      name: "Valentin R",
      birthday: new Date(1997, 8, 13),
    });

    expect(user).toBeDefined();

Création des assertions - 1

Maintenant que nous avons le retour de la fonction "createUser", nous allons devoir vérifier via des assertions que le résultat est bien conforme aux valeurs attendues.

Nous allons utiliser expect pour savoir si l'objet n'est pas "undefined" mais aussi sur les propriétés de l'objet.

Regardons si le retour possède bien un id et qu'il est bien du type "number".

...

describe("User Service", () => {
  it("should create an user", async () => {
    const user = await createUser({
      name: "Valentin R",
      birthday: new Date(1997, 8, 13),
    });

    expect(user).toBeDefined();
    expect(user.id).toBeDefined();
    expect(user.id).toBeTypeOf("number");

Création des assertions - 2

Vérifions que l'objet possède bien "name" et qu'il contient bien le nom de notre user. Ensuite, vérifions si la propriété birthday possède la bonne année et le bon mois.

...

describe("User Service", () => {
  it("should create an user", async () => {
    const user = await createUser({
      name: "Valentin R",
      birthday: new Date(1997, 8, 13),
    });

    expect(user).toBeDefined();
    expect(user.id).toBeDefined();
    expect(user.id).toBeTypeOf("number");
    expect(user).toHaveProperty("name", "Valentin R");
    expect(user.birthday).toBeDefined();
    expect(user.birthday.getFullYear()).toBe(1997);
    expect(user.birthday.getMonth()).toBe(8);

Création des assertions - 3

Enfin, vérifions notre Mock. Le Mock doit avoir été appelé une fois et avec les arguments que nous avons utilisés pour appeler la fonction "createUser".

...

describe("User Service", () => {
  it("should create an user", async () => {
    const user = await createUser({
      name: "Valentin R",
      birthday: new Date(1997, 8, 13),
    });

    expect(user).toBeDefined();
    expect(user.id).toBeDefined();
    expect(user.id).toBeTypeOf("number");
    expect(user).toHaveProperty("name", "Valentin R");
    expect(user.birthday).toBeDefined();
    expect(user.birthday.getFullYear()).toBe(1997);
    expect(user.birthday.getMonth()).toBe(8);
    expect(createUserInRepository).toBeCalledTimes(1);
    expect(createUserInRepository).toBeCalledWith({
      name: "Valentin R",
      birthday: new Date(1997, 8, 13),
    });
  });
});

Création des assertions - 4

Bravo, vous avez réussi votre premier test unitaire.

Vous n'avez même pas besoin de la base de donnée, votre Mock permet d'isoler votre test.

Attention, si vous réutilisez votre Mock pour d'autres tests ensuite, vous devez ajouter dans votre describe un hook "afterEach" pour remettre à zéro le Mock.

Maintenant nous allons voir avec un cas d'erreur, comment peut-on tester les cas où nous savons que notre solution devrait refuser certains comportements ?

describe("User Service", () => {
  afterEach(() => vi.clearAllMocks());
  
  ...

Gestion des erreurs - 1

Créons un nouveau test avec "it" avec la phrase "should trigger a bad request error when user creation". Nous allons vérifier que notre solution renvoie une erreur HttpBadRequest quand nous n'envoyons pas tous les bons éléments.

it("should trigger a bad request error when user creation", async () => {
    try {
      await createUser({
        name: "Valentin R",
      });
      assert.fail("createUser should trigger an error.");
    }

Ajoutons un try/catch autour de "createUser".

Ajoutons une assertion, si l'on arrive à passer la fonction "createUser" alors qu'elle devrait lever une erreur, c'est que notre test a échoué.

Gestion des erreurs - 2

Ajoutons le catch, créons des assertions pour vérifier que l'erreur est bien celle que nous attendons. Nous allons vérifier que son nom est bien une HttpBadRequest et que son statusCode est bien à 400.

it("should trigger a bad request error when user creation", async () => {
    try {
      await createUser({
        name: "Valentin R",
      });
      assert.fail("createUser should trigger an error.");
    } catch (e) {
      expect(e.name).toBe('HttpBadRequest');
      expect(e.statusCode).toBe(400);
    }
  });

Bravo, vous venez de finir votre deuxième test unitaire !

Vérifions ensemble

Appelez-moi pour que je puisse vérifier ces étapes.

 

Maintenant, vous pouvez passer à la phase autonome et faire les exercices.

Cela va être plus compliqué, n'hésitez pas à lire la documentation de Vitest.

Si vous êtes bloqué, appelez-moi pour que je puisse vous donner des conseils.

Exercice 1 : User trop jeune

Maintenant que vous êtes un expert en test unitaire, vous allez devoir réaliser un test permettant de vérifier que notre solution lève bien une erreur quand un utilisateur est trop jeune.

La variable "MAX_USER_AGE" définie déjà l'âge minimum.

 

Appelez-moi à la fin de cet exercice.

Exercice 2 : Comptes bancaires

L'entité User est terminée. Nous allons maintenant développer l'entité "Account" dans le domaine "banking". Vous devez créer les fonctions :

createAccount - qui permet de créer un compte bancaire avec le typage déjà défini dans le fichier init.sql dans le dossier "sql".

getAccounts - qui permet de récupérer les comptes bancaires en fonction de l'id d'un User.

deleteAccount - qui permet de supprimer un compte bancaire en fonction de l'id d'un User et de l'id d'un Account.

Votre code doit suivre les bonnes pratiques déjà mise en place dans la librairie. Je vous conseille de débuter par la création des tests unitaires avant de développer la logique.

Allez à la slide suivante pour la suite de l'exercice.

Exercice 2 : Comptes bancaires

Vous devez réaliser les tests suivants :

  1. createAccount réussi
  2. createAccount échoue avec de mauvais paramètres
  3. getAccounts réussi en vérifiant chaque élément de la liste
  4. deleteAccount réussi
  5. deleteAccount échoue avec un mauvais id d'Account

 

N'oubliez pas de Mock vos tests et de vérifier les bons types d'erreurs.

 

Appelez-moi à la fin de cet exercice.

Exercice 3 : Transferts

L'entité Account est terminée. Nous allons maintenant développer l'entité "Transfer" dans le domaine "banking". Vous devez créer les fonctions :

createTransfer - qui permet de créer un virement avec le typage déjà défini dans le fichier init.sql dans le dossier "sql" et doit mettre à jour les montants dans Account.

getTransfers - qui permet de récupérer les virements en fonction de l'id d'un User.

Vous allez devoir créer des fonctions manquantes chez d'autres entités pour réussir l'exercice. (Ex: patchAccount pour mettre à jour le montant du compte).

 

Allez à la slide suivante pour la suite de l'exercice.

Exercice 3 : Transferts

Vous devez réaliser les tests suivants :

  1. createTransfer réussi
  2. createTransfer échoue avec de mauvais paramètres
  3. createTransfer échoue avec un mauvais montant
  4. createTransfer échoue avec une valeur négative
  5. getTransfers réussi en vérifiant chaque élément de la liste

 

N'oubliez pas de Mock vos tests et de vérifier les bons types d'erreurs.

 

Appelez-moi à la fin de cet exercice.

Exercice 4 : Mock une librairie

Nous allons maintenant ajouter l'entité "Export" dans le nouveau domaine "interoperability".

Nous allons créer la fonction createExport - qui permet de créer un export sous format d'un fichier .xlsx des transferts d'un compte bancaire de notre client.

Nous allons utiliser la librairie node-xlsx pour générer le fichier, cette librairie génère un buffer, vous devez écrire le fichier en utilisant fs de nodejs.

Pour cette fois, nous allons tester ce que nous envoyons à la librairie directement au lieu de Mock le repository, vous devez Mock la méthode .build de la librairie.

 

Appelez-moi à la fin de cet exercice.

Exercice 5 : Coverage

Maintenant que votre librairie est complète et possède de nombreux tests unitaires, il est important de vérifier son taux de couverture pour voir où nous devrions continuer à réaliser des tests pour atteindre 100%.

Mettez en place l'outil de Vitest pour récupérer un rapport de couverture de tests.

 

Réalisez un test manquant pour augmenter le coverage s'il n'est pas encore à 100%.

 

Appelez-moi à la fin de cet exercice.

Exercice 6 : Test d'integration

Maintenant nous allons devoir identifier une fonctionnalité critique de la librairie et nous allons réaliser un test d'integration pour vérifier son bon fonctionnement comme un smoke test.

 

Identifier un élément critique et rédiger un test dans un fichier ".spec.js" pour vérifier la fonctionnalité même avec la base de donnée.

Lancez-la avec Docker avant de réaliser vos tests avec la commande suivante :

 

 

 

Appelez-moi à la fin de cet exercice.

docker run --name postgres -p 5432:5432 \
        -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb \
        -v ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql -d postgres

Exercice 7 : Allons plus loin !

Commencez à découvrir Cypress pour vous préparer aux tests End-to-End. Prenez un projet que vous avez déjà réalisé avec une interface graphique et ajoutez Cypress à ce projet pour créer un test end-to-end.

 

Par exemple, vérifiez qu'un bouton est bien cliquable ou qu'un élément est bien présent sur la page.

 

Appelez-moi quand vous avez terminé.

Exercice 8 : Cypress dans un grand projet

Nous allons ajouter un test au Cypress d'un grand projet Open Source comme EditorJS.

Modifions les tests de l'ajout de lien de l'éditeur dans le fichier :

/test/cypress/tests/inline-tools/link.cy.ts

Ici, nous ajouterons plusieurs tests :

  1. Testons avec le lien http suivant "http://google.com"
  2. Testons avec le lien incomplet suivant "editorjs.io"
  3. Testons en retirant un lien après l'avoir ajouté.
  4. Testons en modifiant un lien après l'avoir ajouté.

 

Appelez-moi quand vous avez terminé.

Les tests

end-to-end

3.

Définition

Le test de bout en bout (ou « end-to-end », ou « E2E » en anglais) est une procédure permettant de vérifier le bon fonctionnement de l'ensemble de l'application logicielle du début à la fin pour s'assurer que tous les composants et dépendances fonctionnent correctement.

Différence avec les tests unitaires

L'objectif d'un test de bout en bout est de simuler des scénarios d'utilisation réels et de vérifier que le système se comporte comme prévu du début à la fin.

L'objectif est contraire aux tests unitaires qui cherchent à isoler la logique métier, ici on cherche à reproduire les mêmes conditions d'un utilisateur final.

Les différents outils

Et d'autres que je vous laisse découvrir, exemple du tag #e2e-testing.

Focus sur Playwright

Cypress et Playwright sont deux très bons choix pour réaliser vos tests end-to-end.

Ici, nous allons choisir Playwright qui est utilisable par plusieurs langages où Cypress n'est utilisable que via Javascript.

Playwright est développé par Microsoft, il évolue donc très vite.

 

Playwright permet d'être utilisable durant le développement et dans une pipeline CI / CD. Il permet de tester Chromium, Firefox et Webkit avec la même API.

TP - Réaliser notre premier test avec Playwright

Installons Playwright

Créons un dossier "E2E" et suivons le Get Started de Playwright pour l'installer et créer notre premier test.

Utilisons la commande, utilisez les paramètres suivants :

npm init playwright@latest

Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
✔ Do you want to use TypeScript or JavaScript? · TypeScript
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true
Initializing NPM project (npm init -y)…

Lançons les tests d'exemples :

npx playwright test​

 

Vous devriez avoir 6 tests réussis. 

Créons un dossier "E2E" et suivons le Get Started de Playwright pour l'installer et créer notre premier test.

Utilisons la commande, utilisez les paramètres suivants :

npm init playwright@latest

Visualisons le rapport de test

Maintenant que nous avons lancé une première fois les tests, un rapport est disponible en utilisant la commande :

npx playwright show-report

Les informations sont concises, il n'y a pas beaucoup de détails, pour aller plus loin nous allons utiliser l'interface graphique pendant nos tests avec la commande :

npx playwright test --ui

Lancez le test "get started link" et survolez les différentes étapes du test pour voir les vues et actions associées sur l'interface testée.

Naviguer entre les pages

Pour notre test end-to-end, nous allons tester un faux site e-commerce, nous utiliserons https://automationexercise.com/.

Nous allons reproduire la navigation d'un client en recherchant un t-shirt pour homme.

Pour cela, créons le fichier "ecommerce.spec.ts" dans le dossier "tests".

Créons une suite de tests et créons notre premier test où nous irons sur la page d'accueil :

import test from "@playwright/test";

test.describe("Ecommerce's product page", () => {
  test("should go to product page", async ({ page }) => {
    await page.goto("https://automationexercise.com/");
  });
});

Sélectionner des éléments et réaliser des actions

Un header existe sur cette page, sélectionnons le lien vers la page des produits.

import test from "@playwright/test";

test.describe("Ecommerce's product page", () => {
  test("should go to product page", async ({ page }) => {
    await page.goto("https://automationexercise.com/");

    // Ceci est un Locator, il permet de localiser un élément de la page
    // https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-label
    // Ici nous localisons le lien vers la page products.
    const nav = page.getByRole("navigation");
  });
});

Sélectionner des éléments et réaliser des actions - 2

Problème, il faut d'abord accepter les cookies pour pouvoir avoir accès au site. Nous allons donc ajouter cette étape, qui est conditionnel puisqu'une fois accepté, cela ne s'affichera pas pour les prochaines fois :

import test, { expect, Page } from "@playwright/test";

async function acceptCookies(page: Page) {
  // On localise le bouton "Accepter les cookies" et on clique dessus
  const acceptCookiesButton = page.getByRole("button", { name: "Consent" });

  // On vérifie si le bouton est visible avant de cliquer
  if (await acceptCookiesButton.isVisible()) {
    await acceptCookiesButton.click();
  }
}

test.describe("Ecommerce's product page", () => {
  // Avant chaque test, on va sur la page d'accueil du site ecommerce
  // et on accepte les cookies
  test.beforeEach(async ({ page }) => {
    // On va sur la page d'accueil du site ecommerce
    await page.goto("https://automationexercise.com/");
    await acceptCookies(page);
  });

  test("should go to product page", async ({ page }) => {
    // Ceci est un Locator, il permet de localiser un élément de la page
    // https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-label
    // Ici nous localisons le lien vers la page products.
    const nav = page.getByRole("navigation");

    await expect(nav).toBeVisible();
  });
});

Sélectionner des éléments et réaliser des actions - 3

Problème à nouveau, le site internet ne suit pas bien les conventions sémantique HTML et l'on ne trouve pas de barre de navigation sur le site. Utilisez l'outil "Pick locator" sur l'interface pour sélectionner le bouton "Product" du site internet :

 

 

 

 

 

Nous allons utiliser le locator pour cliquer directement sur ce bouton et avoir accès à la page des produits.

Sélectionner des éléments et réaliser des actions - 4

Ajoutez les lignes suivantes dans le test pour cliquer sur le bouton et vérifiez que l'on arrive bien sur la bonne page :

  test("should go to product page", async ({ page }) => {
    // Ceci est un Locator, il permet de localiser un élément de la page
    // https://playwright.dev/docs/api/class-framelocator#frame-locator-get-by-label
    // Ici nous localisons le lien vers la page products.
    await page.getByRole("link", { name: " Products" }).click();

    // On vérifie que l'URL de la page est bien celle de la page des produits
    expect(page).toHaveURL("https://automationexercise.com/products");
    // On vérifie que le titre de la page est bien celui de la page des produits
    expect(await page.title()).toBe("Automation Exercise - All Products");
  });

Vérifier la console et le réseau

Lorsque vous lancez vos tests, en bas de Playwright vous avez plusieurs tabs dont le tab Console et Network vous permettant de voir les logs et les requêtes de la page, ici un exemple de la console du site :

Réaliser une recherche

Maintenant, nous allons rechercher comme un utilisateur un t-shirt pour homme. Localisons l'input de recherche et écrivons "t-shirt" puis validons notre recherche :

  test("should find a t-shirt", async ({ page }) => {
    await page.getByRole("link", { name: " Products" }).click();

    // On écrit dans la barre de recherche le mot "t-shirt"
    await page.getByRole("textbox", { name: "Search Product" }).fill("t-shirt");

    // On clique sur le bouton de recherche
    await page.getByRole("button", { name: "" }).click();

    // On vérifie qu'il n'y a que 3 produits affichés
    const products = page.locator(".features_items .product-image-wrapper");

    // On vérifie que le nombre de produits affichés est bien de 3
    await expect(products).toHaveCount(3);
  });

Tester la page produit

Réalisons un troisième test permettant de vérifier le contenu de la page produit :

- Le titre doit être celui du t-shirt polo premium.

- Le prix doit être présent.

- Le bouton d'achat doit être présent.

  test("should contains product's details like title and price", async ({
    page,
  }) => {
    await page.goto("https://automationexercise.com/product_details/30");

    // On vérifie que le titre de la page est bien celle du produit
    expect(await page.title()).toBe("Automation Exercise - Product Details");
    // On vérifie que le titre du produit est bien celui attendu
    await expect(
      page.getByRole("heading", { name: "Premium Polo T-Shirts" })
    ).toBeVisible();
    // On vérifie que le prix du produit est bien présent
    await expect(page.getByText("Rs.")).toBeVisible();
    // On vérifie que le bouton "Add to cart" est bien visible
    await expect(page.getByRole("button", { name: " Add to cart" })).toBeVisible();
  });

TP terminé

Vous avez réalisé vos premiers tests end-to-end.

Vous allez être maintenant face à des exercices en autonomie, n'hésitez pas à lire la documentation de Playwright pour les réussir.

Exercice 1 : Testons le bouton d'achat

Maintenant que vous êtes un expert avec Playwright, vous allez devoir réaliser un test permettant de vérifier que le site possède bien une page panier avec le panier à jour lorsqu'on a ajouté un produit.

 

Vérifiez que les informations du produit sont bien présentes dans le panier.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 2 : Tester un projet

Nous allons arrêter d'utiliser le faux site e-commerce pour voir comment intégrer Playwright à un projet.

Vous allez devoir réaliser un nouveau projet dans un dossier "AUTH-E2E", technologie au choix, où vous allez ajouter Playwright pour tester une interface de création de compte d'utilisateur.

L'inscription est une phase critique dans les applications et nous allons donc vérifier qu'elle est bien fonctionnelle.

Créez une interface simple où vous pouvez créer un compte avec un nom d'utilisateur, un e-mail et un mot de passe.

Réalisez le test de cette interface avec Playwright. 

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 3 : Tester un cas d'erreur

Ajoutez une gestion d'erreur en vérifiant que l'e-mail est bien formaté et que tous les champs sont requis pour créer un compte.

Réalisez le test via Playwright pour vérifier qu'un message d'erreur est bien affiché lorsque l'utilisateur se trompe dans les deux cas :

- L'email n'est pas valide

- Les champs ne sont pas tous remplis

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 4 : Tester une API

Créez une API REST qui gère les comptes de vos utilisateurs sur la route /users et réalisez les tests via Playwright en suivant la documentation.

 

Certains projets nécessitent d'avoir une API publique en plus de l'application, le but ici est de pouvoir tester à la fois les vues de votre application et les routes de votre API publique grâce à Playwright.

 

Appelez-moi pour que l'on puisse valider ensemble.

Exercice 5 : Mise en place des tests dans une CI

Créez une pipeline CI avec Gitlab CI ou Github Actions avec les exercices des tests unitaires et des tests E2E pour les exécuter automatiquement à chaque commit.

 

Appelez-moi pour que l'on puisse valider ensemble.

Test Driven Development

4.

Qu'est-ce que le Test Driven Development ?

Mis en avant par l'ingénieur Kent Beck en 2003 mais déjà présent dans l'XP dès 1999.

Une discipline qui oblige l'écriture de test avant l'écriture du code de production.

Le TDD est une boucle permettant d'avancer petit à petit sur une fonctionnalité.

Les 3 règles du TDD

1. Vous ne pouvez pas écrire de code de production tant que vous n'avez pas écrit un test unitaire qui échoue.

2. Vous ne pouvez pas écrire plus de code de test qu'il n'en faut pour qu'un test unitaire échoue, et ne pas compiler revient à échouer.

3. Vous ne pouvez pas écrire plus de code de production que nécessaire pour que le test unitaire actuellement en échec réussisse.

Cela semble étrange n'est-ce pas ?

Quels sont les avantages du TDD ?

Rédiger la documentation bas-niveau parfaite.

Éviter les trous dans vos tests unitaires, vous êtes serein quand vos tests passent.

Force l'architecture à être maintenable et testable à tout instant.

Double entry bookkeeping

Pour éviter les catastrophes, les comptables réalisent les comptes en partie double

Une partie débitée et une partie créditée, la somme doit être de zéro.

Les tests permettent de décrire précisément le comportement attendu et le code de généraliser.

Double entry bookkeeping

Exemple simple

Une entreprise achète du matériel pour 1 000 € en payant par virement bancaire :

  • Compte "Matériel" (actif) : +1 000 € (débit)
  • Compte "Banque" (actif) : -1 000 € (crédit)

Résultat : l’écriture est équilibrée, ce qui garantit la fiabilité des comptes.

 

C'est le même principe pour le TDD, pour chaque test décrivant le comportement attendu, on aura ses lignes de codes pour réaliser la logique.

La solution sera toujours couverte totalement par les tests.

TP - Réaliser notre première fonctionnalité en TDD

Réalisons une Stack

Nous allons réaliser une stack en Typescript en TDD.

Pour cela, nous allons utiliser Vite et Vitest pour initier le projet et réaliser les tests unitaires dans le dossier "TDD" :

npm create vite@latest

npm install -D vitest

Nous allons alors créer un dossier "test" et y ajouter le fichier "stack.test.ts" qui contiendra nos tests unitaires. Nous allons ensuite créer un fichier dans "src" du nom de "stack.ts" qui contiendra la logique de notre Stack.

Ajoutons dans le "package.json" le script "test" appelant vitest.

  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "test": "vitest"
  },

Réalisons une Stack - 2

Lancez la commande :

npm run test

Vous devriez voir un message d'erreur vous indiquant que vous n'avez pas de tests dans "stack.test.ts", votre première erreur !

On commence alors la boucle du TDD, faisons le nécessaire pour passer au vert. Partons du principe que nous commençons à tester si la Stack possède une propriété permettant de savoir si elle est vide :

describe("Stack class", () => {
  it("should be empty if no values in.", () => {
    const stack = new Stack();

    expect(stack.empty).toBe(true);
  });
});

Réalisons une Stack - 3

Mince, même en sauvegardant, les voyants sont toujours au rouge.

Vous pouvez maintenant appliqué la règle du TDD où ne pas compiler est un échec, on peut donc partir sur notre code de production !

Implémentons la classe Stack et le comportement de la propriété empty :

export class Stack<T> {
  public empty = true;

  constructor() {}
}

Bravo, votre test passe, le voyant est vert !

Mais est-ce vraiment la bonne solution ?

Rédigeons plus de tests pour être sûr...

Réalisons une Stack - 4

Spécifions maintenant que la propriété "empty" devrait être à false si des éléments sont présents dans notre Stack.

  it("should empty to be false with values pushed", () => {
    const stack = new Stack();

    stack.push(1);
    expect(stack.empty).toBe(false);
  });

Maintenant que notre test échoue, allons mettre à jour la logique :

export class Stack<T> {
  public empty = true;

  constructor() {}

  public push(value: T) {
    this.empty = false;
  }
}

Ajoutons la méthode push avec un paramètre générique ne connaissant pas par avance le type de notre Stack.

Réalisons une Stack - 5

Nous avons aussi passé la propriété "empty" à false car nous avons maintenant un élément de plus dans la Stack. Notre test passe !

  it("should empty to be false with values pushed", () => {
    const stack = new Stack();

    stack.push(1);
    expect(stack.empty).toBe(false);
  });

Mais que se passe-t-il si nous enlevons tous les éléments ?

"empty" devrait être de nouveau à false...

  it("should empty to be true if values popped", () => {
    const stack = new Stack();

	stack.push(1);
    stack.pop();
    expect(stack.empty).toBe(true);
  });

Réalisons une Stack - 6

Notre test ne fonctionne plus. Changeons la logique de la Stack.

export class Stack<T> {
  public get empty() {
  	return this.size === 0;
  };
  public size = 0;

  constructor() {}

  public push(value: T) {
    this.size += 1;
  }

  public pop(): T | undefined {
    this.size -= 1;
  }
}

Incroyable, tout est de nouveau au vert !

Alors, vous vous demandez sûrement pourquoi je ne réalise pas une véritable Stack jusqu'ici, pourquoi tourner autour du pot ?

Réalisons une Stack - 7

Ceci est un exemple pour vous montrer que spécifier de plus en plus les tests nous oblige à réaliser le code voulu même en réalisant la solution la plus simple à chaque fois, ici poussé à l'extrême. D'ailleurs en TDD, il vaut mieux se concentrer sur les règles autour du coeur de la logique à tester.

Ajoutons le test pour vérifier que les valeurs en retour de la méthode "pop" ont bien la même valeur que les éléments envoyés à la méthode "push" :

  it("should have same value in the same order after each pop.", () => {
    const value1 = 3;
    const value2 = 55;
    const stack = new Stack();

    stack.push(value1);
    stack.push(value2);
    expect(stack.pop()).toBe(value2);
    expect(stack.pop()).toBe(value1);
  });

Réalisons une Stack - 8

Nous sommes maintenant obligé d'implémenter la Stack pour que ce test puisse passer, bien sûr nous allons refactor :

export class Stack<T> {
  public get empty() {
  	return this._items.length === 0;
  };

  private _items: T[] = [];

  constructor() {}

  public push(value: T) {
    this._items.push(value);
  }

  public pop(): T | undefined {
    return this._items.pop();
  }
}

Bravo, vous avez réalisé votre première fonctionnalité en TDD !

Bien sûr, soyez plus direct lors de l'implémentation du code mais n'oubliez pas de ne jamais produire plus que la résolution du test.

Exercice - Réaliser des Katas en TDD

Pour s'entraîner et vérifier qu'un développeur maitrise certaines techniques, il est possible en entreprise de faire des ateliers où l'on réalise des "Katas".

Comme en Karaté, cela permet de garder en mémoire des techniques.

Voici le lien vers la liste des Katas :

https://github.com/gabbloquet/entrainement-au-tdd?tab=readme-ov-file

Commencez par le Kata FizzBuzz, appelez moi quand vous avez terminé pour vérifier ensemble.

Exercice - Réaliser des Katas en TDD - 2

Continuez à réaliser des Katas, vous êtes libre de choisir ceux à réaliser pour vous entrainer.

Behavior Driven Development

5.

Qu'est-ce que le Behavior Driven Development ?

Utilise le comportement attendu d'une fonctionnalité pour générer les tests.

L'équipe défini des exemples sur le comportement d'une fonctionnalité.

Utilise un langage naturel non-ambigue pour que toute l'équipe puisse créé des exemples.

Quels sont les avantages du BDD ?

Tous les comportements de la solution sont connus et documentés.

Toute l'équipe est capable de rédiger, comprendre et corriger des tests.

Les comportements sont dans les versions de la solution avec le code.

Exemple d'une fonctionnalité

Title: Returns and exchanges go to inventory.

As a store owner,
I want to add items back to inventory when they are returned or exchanged,
so that I can sell them again.

Scenario 1: Items returned for refund should be added to inventory.
Given that a customer previously bought a black sweater from me
And I have three black sweaters in inventory,
When they return the black sweater for a refund,
Then I should have four black sweaters in inventory.

Scenario 2: Exchanged items should be returned to inventory.
Given that a customer previously bought a blue garment from me
And I have two blue garments in inventory
And three black garments in inventory,
When they exchange the blue garment for a black garment,
Then I should have three blue garments in inventory
And two black garments in inventory.

Pourquoi l'Example Mapping ?

Quand vous avez identifié des US, maintenant il vous faut un moyen de facilement rédiger leur fonctionnement et les critères d'acceptation. Pour cela, regrouper autour d'une table le Product Owner, un UX Designer ou un Testeur et un Développeur pour identifier tous les besoins et problématiques.

On appelle cette configuration les "Three amigos!" en référence au film du même nom.

 

Pourquoi ? Pour éviter que seulement une personne avec sa vision métier ne soit responsable de la création des US, là on est sûr d'avoir le point de vu de tout le monde.

Qu'est-ce que l'Example Mapping ?

Exemple d'un atelier

Comment bien rédiger les exemples ?

Pour standardiser et éviter d'avoir des exemples différents pour chacun, on utilise alors le Gherkin. Cela permet de se rapprocher d'une syntaxe plus algorithmique et permet à toute l'équipe de participer à la réflexion de la logique de l'US.

 

Voyons ensemble sa syntaxe et son fonctionnement :

 https://cucumber.io/docs/gherkin/reference/

Exercice - Réaliser un scénario en BDD

Entraînez-vous en réalisant un atelier Example Mapping sur une nouvelle interface de connexion et d'inscription pour vos utilisateurs seul. Réalisez les US suivantes :

1. Utilisateur s'inscrit avec son e-mail.

2. Utilisateur a oublié son mot de passe.

Créez les scénarios en BDD pour une des US en Gherkin.

N'hésitez pas à me poser des questions lorsque vous avez des blocages ou hésitations, Miro est un bon outil pour ce genre d'atelier.

Soutenance

Présentation du projet

6.

7 minutes de présentation + 3 minutes de questions

QCM

30 minutes

7.

N'hésitez pas à me donner votre avis.

Merci!

Annexes - Exemples d'architecture

A.

Un exemple des bonnes pratiques

Voici un projet d'exemple qui met en pratique les différentes bonnes pratiques que l'on a pu voir durant ce module en terme d'architecture et de tests.

Le but est de prendre un cas concret comme ici le projet MyChatGPT où l'on doit créer des conversations ou des messages mais très simple pour rester un exemple.

Ici, on peut voir que nos couches de logique sont bien isolées ce qui nous évite de mélanger nos dépendances techniques avec la logique métier.