Objectif du chapitre¶
Comprendre comment organiser une stratégie de tests automatisés dans un pipeline GitLab CI
- Identifier ce que chaque niveau de test doit prouver
- Choisir le bon niveau de test pour éviter une suite lente et fragile
- Publier les résultats dans GitLab via les rapports de tests et de couverture
- Garder des exemples simples pour illustrer la théorie
La pyramide des tests¶
La pyramide des tests aide à répartir les efforts de validation selon le coût d'exécution et de maintenance
- Tests E2E : Peu nombreux, lents, proches du comportement utilisateur
- Tests d'intégration : Vérifient la collaboration entre plusieurs composants
- Tests unitaires : Nombreux, rapides, isolés, exécutés à chaque changement
Idée clé : plus un test est bas dans la pyramide, plus il doit être rapide, stable et fréquent
Ce que la pyramide évite¶
Sans stratégie claire, une équipe finit souvent avec une suite de tests coûteuse
- Trop de tests E2E pour vérifier des règles métier simples
- Des tests unitaires qui mockent tellement qu'ils ne prouvent plus grand-chose
- Des tests d'intégration instables à cause d'un état partagé
- Une CI lente que les développeurs contournent ou ignorent
Le but n'est pas d'avoir beaucoup de tests, mais d'avoir les bons tests au bon niveau
Vocabulaire commun¶
- Assertion : Vérification du résultat attendu
- Fixture : Donnée de test préparée avant l'exécution
- Mock : Objet simulé qui remplace une dépendance
- Stub : Réponse prédéfinie retournée par une dépendance
- Flaky test : Test dont le résultat varie sans changement de code
Tests unitaires¶
Introduction aux tests unitaires¶
Un test unitaire vérifie le comportement d'une petite unité de code isolée
Caractéristiques¶
- Cible une fonction, une méthode ou un composant simple
- Ne dépend pas d'une base de données, d'un réseau ou d'un service externe
- Utilise des mocks ou stubs uniquement pour isoler les dépendances
- S'exécute en quelques millisecondes
- Donne un feedback immédiat pendant le développement
Outils¶
- Vitest ou Jest pour JavaScript et TypeScript
- Testing Library : Simulation des interactions utilisateur
Structure d'un test unitaire¶
Un test lisible suit souvent le découpage Arrange / Act / Assert
- Arrange : préparer les données et dépendances
- Act : exécuter le comportement testé
- Assert : vérifier le résultat observable
Un bon test unitaire doit échouer pour une seule raison claire
Exemple de test unitaire¶
// price.test.ts
import { expect, test } from 'vitest'
import { priceWithTax } from './price'
test('calcule le prix TTC', () => {
const price = priceWithTax(100, 0.2)
expect(price).toBe(120)
})
Ce test ne lance ni serveur, ni navigateur, ni base de données
Quand utiliser un mock¶
Un mock est utile quand la dépendance est lente, instable ou hors du périmètre du test
- Appel HTTP vers une API externe
- Envoi d'email
- Écriture dans une file de messages
- Lecture d'un secret ou d'une variable d'environnement
À éviter : mocker toutes les fonctions internes de l'application, car le test vérifie alors surtout l'implémentation
Intégration GitLab CI pour les tests unitaires¶
unit_tests:
stage: test
image: node:lts
script:
- npm ci
- npm run test:unit -- --coverage
artifacts:
when: always
paths:
- coverage/
- reports/unit.xml
reports:
junit: reports/unit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
Le rapport JUnit affiche les tests dans GitLab, le rapport Cobertura affiche la couverture
Le framework doit être configuré pour générer reports/unit.xml et coverage/cobertura-coverage.xml
Tests d'intégration¶
Introduction aux tests d'intégration¶
Un test d'intégration vérifie que plusieurs composants fonctionnent correctement ensemble
Exemples¶
- Une API écrit correctement dans une base de données
- Un service publie un message dans une queue
- Un module appelle une API interne avec le bon contrat
- Une couche repository traduit correctement les requêtes métier
Le test d'intégration cherche les erreurs de câblage, de configuration et de contrat
Ce qui change par rapport à l'unitaire¶
- Le test accepte certaines dépendances réelles
- L'environnement est plus lourd à préparer
- Les données doivent être isolées entre deux tests
- Le temps d'exécution est plus élevé
- Le diagnostic est parfois moins immédiat
Le gain principal est la confiance sur les interactions réelles entre composants
Exemple de test d'intégration¶
// users.integration.test.ts
import request from 'supertest'
import { app } from '../src/app'
test('crée un utilisateur via API', async () => {
const response = await request(app)
.post('/users')
.send({ email: 'ada@example.com' })
expect(response.status).toBe(201)
expect(response.body.email).toBe('ada@example.com')
})
Ici, le point d'entrée HTTP est testé avec une partie réelle de l'application
Bases de données en CI¶
Deux approches courantes
- Services GitLab CI : GitLab démarre un conteneur à côté du job
- Testcontainers : Le test démarre lui-même les dépendances Docker nécessaires
Dans les deux cas, il faut prévoir
- Une base dédiée aux tests
- Une stratégie de reset entre les tests
- Des données minimales, explicites et reproductibles
Intégration GitLab CI pour les tests d'intégration¶
integration_tests:
stage: test
image: node:lts
services:
- name: postgres:16
alias: postgres
variables:
DATABASE_URL: 'postgres://postgres:postgres@postgres:5432/app_test'
POSTGRES_PASSWORD: 'postgres'
POSTGRES_DB: 'app_test'
script:
- npm ci
- npm run test:integration
artifacts:
when: always
reports:
junit: reports/integration.xml
Le job reste automatisé, mais il teste une dépendance réelle
Le framework doit être configuré pour générer reports/integration.xml
Tests End-to-End¶
Introduction aux tests E2E¶
Un test end-to-end valide un parcours utilisateur complet sur une application déployée ou démarrée comme en production
Caractéristiques¶
- Traverse le navigateur, le frontend, l'API et la persistance
- Vérifie les parcours critiques, pas toutes les variantes métier
- Reproduit des actions utilisateur réelles
- Produit souvent des captures, traces ou vidéos en cas d'échec
- Coûte plus cher à exécuter et à maintenir
Un test E2E répond à la question : "le système fonctionne-t-il pour l'utilisateur"
Exemples de parcours E2E utiles¶
- Connexion avec un compte valide
- Création d'une ressource importante
- Paiement ou validation de commande
- Inscription et confirmation d'email
- Parcours de lecture critique pour le métier
Les cas limites et règles fines doivent plutôt rester dans les tests unitaires ou d'intégration
Exemple de test E2E avec Playwright¶
// auth.spec.ts
import { expect, test } from '@playwright/test'
test('un utilisateur peut se connecter', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('secret')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page).toHaveURL('/dashboard')
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
})
Les sélecteurs orientés accessibilité rendent le test plus proche du comportement utilisateur
Anti-patterns E2E¶
- Utiliser
sleepau lieu d'attendre un élément ou un état - Tester tous les cas métier via le navigateur
- Dépendre d'un ordre d'exécution entre tests
- Réutiliser un état partagé non maîtrisé
- Cliquer sur des sélecteurs CSS trop liés à l'implémentation
Un test E2E doit rester court, lisible et centré sur un parcours critique
Intégration GitLab CI pour les tests E2E¶
e2e_tests:
stage: e2e
image: mcr.microsoft.com/playwright:v1.60.0-noble
variables:
PLAYWRIGHT_JUNIT_OUTPUT_NAME: test-results/e2e.xml
script:
- npm ci
- npx playwright test --reporter=html,junit
artifacts:
when: always
paths:
- playwright-report/
- test-results/
reports:
junit: test-results/e2e.xml
Les rapports HTML, traces et captures aident à comprendre les échecs en CI
Automatisation dans GitLab CI¶
Organisation des stages¶
stages:
- test
- e2e
unit_tests:
stage: test
script:
- npm run test:unit
integration_tests:
stage: test
script:
- npm run test:integration
e2e_tests:
stage: e2e
script:
- npm run test:e2e
Les tests rapides peuvent tourner en parallèle dans le même stage, les E2E passent ensuite
Où placer les tests dans le workflow¶
- À chaque push : tests unitaires et lint
- À chaque merge request : unitaires, intégration, couverture, rapports JUnit
- Avant merge vers main : E2E sur les parcours critiques
- Après déploiement staging : E2E plus complets sur une application réelle
- Planifié la nuit : tests longs, cross-browser, charge, suites non bloquantes
Plus le feedback est rapide, plus il est utile au développeur
Gestion des résultats dans GitLab¶
GitLab peut exploiter les artifacts pour rendre la CI lisible dans l'interface
- JUnit : liste des tests passés, échoués ou ignorés
- Cobertura : couverture de code affichée dans les merge requests
- Artifacts HTML : rapports Playwright, Cypress, coverage HTML
- Artifacts on failure : logs, captures, traces et vidéos uniquement en cas d'échec
Un job de test doit produire des informations exploitables, pas seulement un statut rouge
Critères de qualité¶
Une bonne suite de tests automatisés est
- Rapide : le feedback arrive avant que le contexte soit perdu
- Déterministe : même code, même résultat
- Lisible : l'échec indique clairement le comportement cassé
- Maintenable : le test résiste aux refactorings internes
- Observable : la CI conserve les rapports utiles au diagnostic
La couverture est un indicateur, pas une preuve de qualité
Récap¶
- Les tests unitaires protègent la logique métier rapidement
- Les tests d'intégration valident les contrats et le câblage entre composants
- Les tests E2E protègent les parcours utilisateur critiques
- GitLab CI automatise l'exécution et publie les résultats via les artifacts
- Une bonne stratégie réduit le coût du feedback sans sacrifier la confiance