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.
Enjeux des tests automatisés et tests unitaires
Test Driven Development
Soutenance de fin de module
Tests end-to-end
Behavior Driven Development
Chaque séance débutera par la présentation d'un concept et de l'intérêt d'utilisation de celui-ci.
Après la théorie, nous verrons alors la pratique en réalisant des exercices.
Nous verrons ensemble la correction des travaux pratiques. N'hésitez pas à poser vos questions.
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
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
Réduire le temps et le coût des tests
Éviter les erreurs humaines des tests manuels
Éviter les régressions à la source
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.
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.
Domain Driven Design
Behavior Driven Development
Test Driven Development
Domain Driven Design
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.
Cela permet aux développeurs de gérer les régressions à chaque commit et de pouvoir avoir des rapports d'erreurs complets.
Votre temps est limité
Certains tests ne sont pas utiles
Perte d'intérêt si les tests durent trop longtemps à chaque itération
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 »).
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.
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.
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.
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 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.
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
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.
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.
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", () => {
...
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", () => {
...
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();
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");
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);
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),
});
});
});
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());
...
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é.
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 !
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.
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.
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.
Vous devez réaliser les tests suivants :
N'oubliez pas de Mock vos tests et de vérifier les bons types d'erreurs.
Appelez-moi à la fin de cet exercice.
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.
Vous devez réaliser les tests suivants :
N'oubliez pas de Mock vos tests et de vérifier les bons types d'erreurs.
Appelez-moi à la fin de cet exercice.
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.
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.
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
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é.
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 :
Appelez-moi quand vous avez terminé.
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.
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.
Et d'autres que je vous laisse découvrir, exemple du tag #e2e-testing.
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.
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
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.
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/");
});
});
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");
});
});
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();
});
});
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.
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");
});
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 :
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);
});
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();
});
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.
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.
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.
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.
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.
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.
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é.
|
|
|
Cela semble étrange n'est-ce pas ?
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.
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.
Une entreprise achète du matériel pour 1 000 € en payant par virement bancaire :
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.
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"
},
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);
});
});
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...
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.
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);
});
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 ?
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);
});
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.
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.
Continuez à réaliser des Katas, vous êtes libre de choisir ceux à réaliser pour vous entrainer.
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.
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.
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.
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.
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 :
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.
Présentation du projet
7 minutes de présentation + 3 minutes de questions
30 minutes
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.