Image de couverture - Différences entre Test Unitaire, d'Intégration et E2E

Différences entre Test Unitaire, d'Intégration et E2E

Découvrez les différences entre tests unitaires, d'intégration et E2E. Optimisez votre code et fiabilisez vos applications grâce à une stratégie de tests adaptée.

Icône de calendrier
Icône de chronomètre 8 min

Dans le monde du développement, les tests sont essentiels pour garantir la qualité, la stabilité et la sécurité du code. Pourtant, avec tant de types de tests disponibles, il est facile de se perdre ! L’objectif de cet article est de vous initier aux 3 types de tests fonctionnels les plus répandus en développement web : test unitaire, test d’intégration et test E2E (End-to-End).

Pourquoi écrire des tests ?

Dans le développement web, les tests jouent un rôle crucial dans l’assurance qualité des applications.

Ils permettent de vérifier que le code fonctionne correctement lorsqu’il est mis à jour. Ces tests sont l’unique garantie de la stabilité d’une application et sont donc d’incontournables alliés pour la maintenance.

La mise en place de tests, qu’ils soient unitaires, d’intégration ou E2E, est essentielle pour éviter les bugs en production et fournir une expérience utilisateur fiable.

Bien que la mise en place d’un workflow de tests puisse sembler chronophage, outre la limitation des risques d’erreurs critiques, établir une stratégie de tests bien pensée contribue sur le long terme, à gagner du temps.

3 principaux types de tests

Lorsqu’il s’agit de vérifier la robustesse d’une application, trois types de tests se démarquent : les tests unitaires, les tests d’intégration et les tests E2E.

Explorons les caractéristiques de ces types de tests, accompagnées d’exemples concrets pour appréhender leur application dans vos projets.

Test unitaire

Rôle

Le rôle des tests unitaires est de vérifier le fonctionnement de chaque fonction ou méthode de manière isolée pour s’assurer qu’elles produisent le résultat attendu, sans influence extérieure.

Les tests unitaires ciblent une logique précise et simple, ce qui aide à identifier rapidement les sources d’erreur dans le code.

Les tests unitaires sont réalisés directement dans le code.

En général, chaque test est indépendant, ce qui garantit que les modifications ou erreurs dans une fonction n’impactent pas les autres.

Exemple concret

Prenons l’exemple d’une fonction calculant le prix d’un café, auquel il est possible d’ajouter des suppléments.

Voici les prix fixés par supplément :

  • noisette : 0.50 €
  • caramel : 1.00 €
  • chantilly : 1.00 €

Considérons la fonction calculerPrixCafe() suivante :

FONCTION calculerPrixCafe(prixBase, supplements)
  // Tableau associatif des prix des suppléments
  PRIX_SUPPLEMENTS = {
    "noisette" : 0.5,
    "caramel" : 1,
    "chantilly" : 1,
  }

  // Vérifier que prixBase est un nombre positif
  SI TYPE_DE(prixBase) != "nombre" OU prixBase <= 0 ALORS
    RETOURNER "Erreur : le prix de base doit être un nombre positif."
  FIN SI

  // Vérifier que supplements est un tableau
  SI TYPE_DE(supplements) != "tableau" ALORS
    RETOURNER "Erreur : supplements doit être un tableau."
  FIN SI

  // Vérifier que chaque élément du tableau est un supplément valide
  POUR CHAQUE supplement DANS supplements FAIRE
    SI supplement PAS_DANS PRIX_SUPPLEMENTS ALORS
      RETOURNER "Erreur : supplément inconnu - " + supplement
    FIN SI
  FIN POUR

  prixTotal = prixBase

  // Calcul du prix des suppléments
  POUR CHAQUE supplement DANS supplements FAIRE
    SI supplement DANS PRIX_SUPPLEMENTS ALORS
      prixTotal = prixTotal + PRIX_SUPPLEMENTS[supplement]
    SINON
      AFFICHER "Supplément inconnu : " + supplement
    FIN SI
  FIN POUR

  RETOURNER prixTotal
FIN FONCTION

Un test unitaire doit donc vérifier que, pour une valeur donnée, la fonction calcule bien le prix du café. On isole donc cette fonction et on simule des entrées pour observer si elle produit les valeurs attendues.

Test unitaire calculerPrixCafe() :

// 🧪 Test unitaire : sans supplément
VERIFIER(calculerPrixCafe(2) = 2)

// 🧪 Test unitaire : avec suppléments
VERIFIER(calculerPrixCafe(2, ["noisette"]) = 2.5)
VERIFIER(calculerPrixCafe(2, ["caramel"]) = 3)
VERIFIER(calculerPrixCafe(2, ["chantilly"]) = 3)
VERIFIER(calculerPrixCafe(2, ["noisette", "chantilly"]) = 3.5)
	
// 🧪 Tests unitaires : entrées invalides
VERIFIER(calculerPrixCafe(-1) = "Erreur : le prix de base doit être un nombre positif.")
VERIFIER(calculerPrixCafe(0) = "Erreur : le prix de base doit être un nombre positif.")
VERIFIER(calculerPrixCafe("50") = "Erreur : le prix de base doit être un nombre positif.")
VERIFIER(calculerPrixCafe(null) = "Erreur : le prix de base doit être un nombre positif.")
VERIFIER(calculerPrixCafe(2, "wasabi") = "Erreur : supplements doit être un tableau.")
VERIFIER(calculerPrixCafe(2, ["wasabi"]) = "Erreur : supplément inconnu - wasabi")

Outils de tests unitaires

Les outils permettant de faciliter l’intégration de tests unitaires au sein d’une application sont nommés frameworks de tests. Parmi les plus célèbres d’entre eux, on retrouve notamment :

Ces frameworks permettent d’écrire des tests rapides et simples à intégrer dans un pipeline CI/CD.

Test d’intégration

Rôle

Le rôle des tests d’intégration est de valider l’interaction entre plusieurs modules ou composants d’une application pour s’assurer qu’ils fonctionnent bien ensemble.

Contrairement aux tests unitaires, qui isolent chaque fonction ou méthode, les tests d’intégration vérifient la communication et la compatibilité entre différentes parties du code.

Les tests d’intégration aident à identifier les erreurs de coordination, comme les problèmes d’interaction entre composants. Ces tests sont réalisés sur des systèmes où plusieurs modules (fonctions, base de données, APIs…) sont connectés et dépendent les uns des autres.

Exemple concret

Reprenons l’exemple de notre interface permettant de commander plusieurs cafés, mais cette fois-ci, intéressons-nous à la mécanique de commande dans sa globalité, impliquant de :

  • Calculer le prix des cafés
  • Mettre à jour les stocks

Considérons la fonction commanderBoissons() suivante :

FONCTION commanderBoissons(boissons)
  total = 0

  POUR CHAQUE boisson DANS boissons FAIRE
    // Vérifier le stock de la boisson
    SI stock >= 1 ALORS
      // Ajouter le prix de la boisson au total
      total = total + calculerPrix(boisson.base, boisson.supplement)
      // Réduire le stock de la boisson
      stock = stock - 1
    SINON
      // Gérer le cas où le stock est insuffisant
      AFFICHER "Stock insuffisant pour " + boisson.nom
    FIN SI
  FIN POUR

  RETOURNER total
FIN FONCTION

Test d’intégration : commanderBoissons() :

boissons = [
  { base: 2 },
  { base: 2, supplement: ["noisette", "chantilly"] }
]

stock = 3

montant = commanderBoisson(boissons)

// 🧪 Test d'intégration
VERIFIER(
  montant = 5.5 ET
  stock = 1
);

Il ne s’agit pas d’un test unitaire mais bien d’un test d’intégration car c’est la mécanique de commande qui est testée dans son ensemble. Le test vérifie que les différentes unités ou modules du système interagissent correctement.

  • commanderBoissons() : Cette fonction gère la commande de boissons, vérifie le stock, calcule le prix total, et réduit le stock.
  • calculerPrix() : Cette fonction est appelée pour calculer le prix de chaque boisson.
  • Variable stock : Cette variable contient le stock de boissons disponibles.

Ainsi, ce test d’intégration permet de vérifier plusieurs mécanismes :

  1. Interactions entre composants : Le test vérifie que la fonction commanderBoissons() interagit correctement avec la variable globale stock et la fonction calculerPrix().
  2. État partagé : Le test vérifie que l’état partagé stock est correctement mis à jour après l’exécution de la fonction commanderBoissons().
  3. Résultat global : Le test vérifie que le montant de la commande montant est correct.

Outils de tests d’intégration

Au-delà de simples test unitaires, les frameworks de tests tels que Jest, Mocha, Vitest, JUnit ou encore PHPUnit proposent des fonctionnalités robustes pour la création et l’exécution de tests d’intégration.

Test E2E (End-to-End)

Rôle

Le rôle des tests E2E (« End-to-End » ou en français « de bout en bout ») est de vérifier le fonctionnement global de l’application du point de vue de l’utilisateur final.

Ces tests simulent des scénarios d’utilisation réels et vérifient que toutes les parties de l’application fonctionnent correctement ensemble. Il peut par exemple s’agir de :

  • Commande d’un produit : Vérifier le parcours complet d’achat, de la recherche au paiement, pour assurer la fluidité.
  • Formulaire de contact : Tester la soumission et validation du formulaire pour garantir l’envoi des informations.
  • Inscription et connexion : S’assurer que l’utilisateur peut créer un compte et se connecter facilement.
  • Gestion du panier : Valider l’ajout, la modification et la suppression d’articles pour un processus d’achat cohérent.
  • Etc.

Réaliser des tests E2E (End-to-End) implique de simuler des interactions utilisateur réelles avec l’application… autrement dit, il est nécessaire de pouvoir déclencher des actions à travers l’interface utilisateur (UI) :

  • Ouvrir un navigateur / Une application
  • Accéder à une URL
  • Cliquer sur un élément de la page
  • Ecrire dans un champ de formulaire
  • Etc.

Exemple concret

Gardons l’exemple de notre interface de commande de café. Un test E2E consisterait par exemple à simuler le parcours complet de commande de café par un utilisateur.

Test E2E : Commande de café avec suppléments :

// 1. 👉  Ouverture du navigateur
ouvrirNavigateur("http://monapp.com/commander")
// 🧪 Test E2E
VERIFIER(window.open(true))

// 2. 👉  Sélectionner un café
cliquerSur("bouton-selectionner-cafe")
// 🧪 Test E2E
VERIFIER(window.contientTexte("Votre café a été ajouté au panier"))
VERIFIER(panier.contientTexte("Café x1"))

// 3. 👉 Ajouter 2 suppléments
cliquerSur("option-supplement-noisette")
cliquerSur("option-supplement-chantilly")
// 🧪 Test E2E
VERIFIER(panier.contientTexte("Options (café 1) : noisette, chantilly"))

// 4. 👉 Sélectionner un second café
cliquerSur("bouton-selectionner-cafe")
// 🧪 Test E2E
VERIFIER(window.contientTexte("Votre café a été ajouté au panier"))
VERIFIER(panier.contientTexte("Café x2"))

// 5. 👉 Finaliser la commande
cliquerSur("bouton-finaliser-commande")
// 🧪 Test E2E
VERIFIER(window.contientTexte("Merci pour votre commande"))

// 🧪 Test E2E : état final de l'application
VERIFIER(
  stock == 39 &&
  commande.total == 5.5 &&
  commande.items.contient("noisette", "chantilly")
)

Outils de tests E2E

Parmi les plus célèbres outils permettant d’exécuter des tests E2E, on retrouve notamment :

Ces outils permettent d’écrire des tests E2E qui simulent des interactions utilisateur réelles et vérifient que l’application fonctionne correctement de bout en bout.

Comparatif : Test Unitaire VS Test d’Intégration VS Test E2E

Les tests jouent chacun un rôle clé pour garantir la qualité et la robustesse des applications. Voici un tableau comparatif mettant en lumière les caractéristiques de chacun de ces types de tests :

CritèresTest unitaireTest d’intégrationTest E2E
EnvironnementCode (exécution isolée d’une fonction)Code (exécution d’interaction entre modules)Environnement utilisateur (navigateur, app mobile…)
Exemples d’outilsJest, Vitest, Mocha, JUnit, PHPUnitJest, Vitest, Mocha, JUnit, PHPUnitCypress, Selenium, Playwright
Mise en placeRapideMoyenne - nécessite des dépendances contrôléesPlus longue - configuration réaliste complexe
RéalismeBas - teste des éléments isolésMoyen - teste des interactions spécifiquesÉlevé - simule des scénarios réels
IsolationTrès isolé (composants individuels)Partiellement isolé (groupes de modules)Non isolé - système complet

En résumé, les tests unitaires vérifient les composants individuels, les tests d’intégration valident les interactions entre modules, et les tests E2E assurent que les parcours utilisateur complets fonctionnent de bout en bout.

Lire aussi