00 - Les tests dans un pipeline CI/CD
Ce que tu vas apprendre
- Pourquoi les tests locaux ne suffisent pas
- Comment configurer GitLab CI pour lancer
bun test - Paralleliser, cacher, échouer vite
- Sauvegarder les rapports de coverage et les screenshots
Prerequisites
Avoir lu la serie Tests fondamentaux, en particulier les articles sur les tests unitaires et d'intégration. Avoir un projet avec GitLab CI configure (voir la serie déploiement GitLab).
"Ca marche sur ma machine"
Je ne compte plus le nombre de fois ou un de mes deux juniors m'a dit "mais ca passe chez moi". Bien sur que ca passe chez toi. Tu as Node 22, Bun 1.2, une base PostgreSQL locale avec les bonnes donnees de seed, et ton .env est configure aux petits oignons depuis 6 mois.
Sauf que la prod, c'est pas ta machine. Et le PC de ton collegue non plus.
Les tests qui tournent uniquement en local ont un problème fondamental : personne ne vérifié qu'ils passent avant de merger. Sur paltemps.fr, avant d'avoir la CI, on avait une regle orale : "lance les tests avant de push". Devinez combien de fois c'etait respecte ? A peu pres une fois sur trois.
Un pipeline CI/CD resout ca. Chaque push déclenché les tests automatiquement. Pas de negociation, pas d'oubli, pas de "je le ferai apres". Le code ne merge pas si les tests echouent.
Le job de test minimal
Voici le strict minimum pour lancer bun test dans GitLab CI :
yamltest:
stage: test
image: oven/bun:1
script:
- cd api && bun install --frozen-lockfile
- bun test --coverage
artifacts:
when: always
paths:
- api/coverage/
Quelques points a noter. --frozen-lockfile empeche bun de modifier le lockfile en CI. Si les dépendances ne matchent pas, le job echoue -- c'est voulu. --coverage généré un rapport de couverture. Et when: always sur les artifacts, c'est pour récupérer le rapport meme si les tests echouent.
Paralleliser les tests
Un seul job qui lance tout, ca marche au début. Quand tu as 400 tests et que le job prend 8 minutes, tu vas vouloir découper. Sur paltemps.fr, on a séparé en trois jobs :
yamlstages:
- lint
- test
- e2e
- deploy
lint:
stage: lint
image: oven/bun:1
script:
- cd api && bun install --frozen-lockfile
- bunx biome check .
test:unit:
stage: test
image: oven/bun:1
script:
- cd api && bun install --frozen-lockfile
- bun test --filter '*.test.ts' --bail
artifacts:
when: always
reports:
junit: api/test-results.xml
test:integration:
stage: test
image: oven/bun:1
services:
- postgres:16
variables:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
DATABASE_URL: "postgresql://test:test@postgres:5432/test"
script:
- cd api && bun install --frozen-lockfile
- bun test --filter '*.integration.test.ts'
test:e2e:
stage: e2e
image: mcr.microsoft.com/playwright:v1.50.0
script:
- cd api && bun install --frozen-lockfile
- bunx playwright test
artifacts:
when: on_failure
paths:
- api/test-results/
- api/playwright-report/
Les jobs test:unit et test:integration tournent en parallèle (meme stage). Les e2e arrivent apres, parce qu'ils sont plus lents et qu'on veut d'abord s'assurer que les bases fonctionnent.
L'ordre complet du pipeline : lint -> unit tests + intégration tests -> e2e -> deploy. Si le lint echoue, on ne perd pas de temps a lancer les tests.
Cacher les dépendances
Sans cache, chaque job telecharge toutes les dépendances a chaque run. Sur un monolithe avec 200 packages, ca ajoute 30 secondes par job. La solution :
yaml.bun-cache:
cache:
key:
files:
- api/bun.lockb
paths:
- api/node_modules/
- ~/.bun/install/cache/
test:unit:
extends: .bun-cache
# ... reste du job
La clé de cache est basee sur le lockfile. Si les dépendances changent, le cache se régénéré automatiquement. Simon il est réutilisé entre les pipelines.
Échouer vite avec --bail
Quand un test echoue dans la CI, inutile de lancer les 399 tests restants. Le flag --bail arrêté l'exécution au premier échec :
bashbun test --bail
C'est particulièrement utile sur les branches de feature. Tu sais deja que c'est casse, autant economiser les minutes de CI. Sur la branche main par contre, je préféré lancer tous les tests pour avoir une vision complète.
Coverage badge et seuil minimum
Le rapport de coverage c'est bien, mais si personne ne le regarde ca ne sert a rien. Deux astuces :
Première astuce, ajouter un badge de coverage dans le README du projet. GitLab peut parser la sortie de bun test --coverage avec un regex :
yamltest:unit:
# ...
coverage: '/All files\s*\|\s*(\d+\.?\d*)\s*\|/'
Deuxieme astuce, faire échouer le job si la coverage tombe en dessous d'un seuil. Pas de 100% (voir l'article 02 sur le piège du coverage), mais un minimum raisonnable :
typescript// bunfig.toml
[test]
coverage = true
coverageThreshold = { line = 60, function = 60 }
Les artifacts qui sauvent la vie
Les artifacts les plus utiles en CI :
- Rapport de coverage : HTML généré par
bun test --coverage, consultable directement dans GitLab - Screenshots Playwright : quand un test e2e echoue, le screenshot montre exactement ce que le navigateur affichait
- Rapport JUnit : GitLab affiche les résultats directement dans la merge request
Le when: on_failure sur les artifacts e2e évité de stocker des screenshots quand tout va bien. On ne garde que ceux des échecs, qui sont les seuls utiles.
Ce que ca change au quotidien
Depuis que le pipeline est en place sur paltemps.fr, les merge requests qui cassent quelque chose sont bloquees avant review. Mes deux juniors ne peuvent plus merger du code qui fait échouer les tests. Ca parait autoritaire dit comme ca, mais en pratique ca les a rassures. Ils savent que s'ils cassent quelque chose, la CI les previent avant que ca arrive en prod.
Le temps total du pipeline est de 3 minutes. C'est assez rapide pour ne pas bloquer le flow de travail, et assez rigoureux pour attraper les regressions.
Article suivant : 01 - Quelle stratégie de test pour quel projet