Une suite de tests qui prend 15 minutes en intégration continue est une suite que personne ne lance localement avant de pousser. Une suite qui rend son verdict en 3 minutes change le rapport au code : on commit, on attend, on vérifie, on merge. Le levier principal pour passer de 15 a 3 minutes est la parallélisation. Ce tutoriel construit pas-à-pas une CI GitHub Actions parallélisée pour Vitest et Playwright, avec sharding, cache des binaires, fusion des rapports et matrice multi-OS quand c’est utile.
Prerequis
- Un dépôt GitHub (les principes valent aussi pour GitLab CI et CircleCI avec une syntaxe différente).
- Vitest et/ou Playwright déjà installés dans le projet.
- Node.js 20 ou plus sur la machine de développement.
- Niveau attendu : connaissance YAML de base, lecture de pipelines existants.
- Temps total : 2h pour parcourir et adapter au projet.
Étape 1 — Ecrire un premier workflow non parallélisé
Avant d optimiser, on démarre par une CI fonctionnelle qui exécute les tests sequentiellement. C est la baseline qu’on cherchera a accélérer. Le workflow vit dans .github/workflows/ci.yml.
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test:run
- run: npx playwright test
Cette baseline fait tout le travail dans un seul job. actions/checkout@v5 recupere le code, actions/setup-node@v5 installé Node 24 (LTS Active en 2026) avec le cache npm pour éviter de retelecharger les dépendances. npx playwright install --with-deps chromium telecharge le binaire Chromium et les dépendances système. Le tout finit en 8 a 12 minutes selon la taille du projet.
Étape 2 — Séparer les jobs unit et e2e
Le premier gain vient en separant les tests rapides des tests lents. Les tests Vitest tournent souvent en moins de 60 secondes ; les tests Playwright peuvent prendre 5 a 10 minutes. Faire les deux dans le même job force a attendre que tout finisse pour avoir un retour. En les separant, on a le verdict unitaire en moins d’une minute, ce qui accélère les retours en PR.
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 24, cache: npm }
- run: npm ci
- run: npm run test:run
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 24, cache: npm }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
Les deux jobs s’exécutent en parallèle. Sur un compte gratuit, GitHub Actions accorde plusieurs runners simultanes, donc le temps total devient le max des deux jobs (souvent l e2e). Sur une PR, on voit dans l interface deux statuts distincts : un check unit, un check e2e. C est plus lisible pour l auteur et plus actionnable.
Étape 3 — Sharder Playwright en 4 morceaux
Playwright supporte le sharding natif via l option --shard. La suite est découpée en N parts egales, chaque shard tourne sur un runner different. Avec 4 shards, une suite de 12 minutes tombe a environ 3 minutes plus le temps d amorcage de chaque runner.
e2e:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 24, cache: npm }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}/4
- uses: actions/upload-artifact@v7
if: always()
with:
name: playwright-blob-report-${{ matrix.shard }}
path: blob-report
retention-days: 7
La directive strategy.matrix.shard: [1, 2, 3, 4] déclenche quatre exécutions parallèles du job, chacune avec une valeur différente de matrix.shard. Playwright lit cette valeur et exécute uniquement les tests de son shard. Le fail-fast: false empêche GitHub d annuler les autres shards si un échoue : on veut voir tous les échecs en même temps, pas un par un. Chaque shard sauvegarde son blob report comme artifact, ce qui permet de fusionner les rapports plus tard.
Étape 4 — Fusionner les rapports en un seul HTML
Quatre rapports séparés sont moins pratiques qu un seul rapport unifie. Playwright fournit une commande merge-reports qui agrege les blob reports en un rapport HTML.
Prerequis pour la fusion : activer le blob reporter dans playwright.config.ts avant que les shards ne tournent. Ce reporter cree le dossier blob-report/ que les jobs uploadent comme artifact.
// playwright.config.ts (extrait)
export default defineConfig({
reporter: process.env.CI ? 'blob' : 'html',
});
Sans cette ligne, le dossier blob-report/ n existe pas a la fin du run et l’upload est vide. Avec elle, chaque shard écrit son blob et la fusion en aval consolide le tout.
merge-reports:
if: always()
needs: e2e
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 24, cache: npm }
- run: npm ci
- uses: actions/download-artifact@v8
with:
path: all-blob-reports
pattern: playwright-blob-report-*
merge-multiple: true
- run: npx playwright merge-reports --reporter html ./all-blob-reports
- uses: actions/upload-artifact@v7
with:
name: playwright-html-report
path: playwright-report
retention-days: 14
Le job merge-reports attend que les quatre shards finissent (needs: e2e) puis telecharge tous les artifacts contenant un blob report, fusionne le tout, et publie un rapport HTML unifie. Le if: always() garantit que la fusion s exécute même si certains shards échouent — c’est exactement quand on a le plus besoin du rapport. L équipe telecharge l artifact en un clic depuis l interface GitHub.
Étape 5 — Cacher les binaires Playwright
L’installation des binaires Playwright prend 30 a 60 secondes par run et telecharge environ 200 Mo. Sur quatre shards qui font tous la même chose, c’est du gaspillage. La parade est un cache GitHub Actions sur le dossier ou Playwright stocké ses binaires.
- name: Cache Playwright browsers
id: pw-cache
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- name: Install Playwright browsers
if: steps.pw-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
La clé de cache est constituée de l OS du runner et du hash de package-lock.json (qui change quand la version Playwright change). Si le cache est présent, on saute l installation ; sinon, on l exécute. Sur un projet stable, l installation Playwright ne se reproduit qu une fois par changement de version. Gain typique : 30 a 60 secondes par shard, soit 2 a 4 minutes economisees par exécution sur 4 shards.
Étape 6 — Activer le test Vitest en parallèle
Vitest parallélisé par défaut sur les coeurs disponibles, ce qui est generalement suffisant. Mais sur un runner GitHub Actions standard (4 vCPU), la parallélisation profite seulement aux suites de plus de 200 fichiers de test. Pour les projets plus petits, le gain est marginal.
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 24, cache: npm }
- run: npm ci
- run: npm run test:run -- --reporter=junit --outputFile=test-results.junit.xml
- uses: actions/upload-artifact@v7
if: always()
with: { name: vitest-junit, path: test-results.junit.xml }
Le format JUnit XML est universellement consomme par les outils de reporting et de statistiques (Datadog Test Visibility, GitHub Test Reporter). Il est inutile pour la lecture humaine mais essentiel pour l observabilité des tests dans le temps. Conservez cet artifact 14 jours et vous disposez d’une vue de l évolution de la durée des tests sur les deux dernières semaines.
Étape 7 — Ajouter une matrice OS quand pertinent
Pour un projet web qui ne touche pas au système de fichiers ni aux processus natifs, tester sur Ubuntu suffit. Pour une CLI ou une bibliothèque qui supporte Windows et macOS, il faut prouver qu’elle marche sur les trois. La matrice OS coûte cher (chaque OS triple le temps), donc on l active uniquement la ou elle a du sens.
cross-platform:
if: github.ref == 'refs/heads/main'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 24, cache: npm }
- run: npm ci
- run: npm run test:run
Le job ne se déclenche que sur les pushes sur main, pas sur chaque PR — ce qui évite de tripler le coût pour des changements qui n affectent pas la portabilité. C est un compromis pragmatique : on détecté les regressions cross-platform avant le release, sans payer le coût sur chaque PR de feature.
Étape 8 — Vérifier le pipeline en bout en bout
On commit le workflow, on ouvre une PR, et on observé ce qui se passe dans l’onglet Actions de GitHub. Quatre shards apparaissent en parallèle, le job unit en parallèle, puis le job merge-reports après la fin des shards.
git add .github/workflows/ci.yml
git commit -m "ci: add parallel Playwright tests with 4 shards"
git push -u origin chore/ci-parallel
Après push, l interface Actions montre la progression en temps réel. Sur une suite typique, on observé une durée totale autour de 3 a 4 minutes, contre 12 a 15 minutes auparavant. La première exécution est toujours plus longue (cache vide), les suivantes profitent du cache npm et du cache Playwright pour finir encore plus vite.
Erreurs fréquentes
| Symptome | Cause probable | Solution |
|---|---|---|
| Shards qui se marchent dessus dans la base | Base de test partagée | Une base par shard via Docker, ou suffixe aleatoire sur les emails de test |
| Cache Playwright qui ne se remet jamais a jour | Cle de cache trop générique | Inclure hashFiles('**/package-lock.json') dans la clé |
| npm ci qui prend 2 minutes | Cache npm absent | Activer cache: npm dans setup-node |
| Rapport HTML manquant a la fin | Job merge non déclenche | Ajouter if: always() et needs: e2e sur le job de fusion |
| Tests qui passent en local, échouent en CI | Variables d environnement manquantes | Vérifier secrets et env au niveau du job |
Tutoriels associes
- Tests E2E avec Playwright en 2026 : tutoriel pas-à-pas — installation, locators, auto-attente et trace viewer.
- Tests unitaires avec Vitest en 2026 : tutoriel pas-à-pas — premiers tests, matchers, mocks et coverage v8.
- Tests visuels avec Playwright en 2026 : tutoriel pas-à-pas — snapshots stables, masquage des zones dynamiques et Docker officiel.
Bonnes pratiques approfondies
Au-delà de la mécanique de sharding et de cache, quelques pratiques distinguent une CI durable d’une CI qui devient lentement inutilisable. La première est l établissement d’un budget temps explicite. L équipe se met d accord : pas plus de 5 minutes du push au verdict sur une PR. Si on dépasse, on agit (sharder davantage, scinder les jobs, optimiser les tests lents) avant que personne n attende plus le retour. Sans budget explicite, la CI grossit insidieusement et la confiance s erode.
La deuxième pratique est l observabilité. Le rapport JUnit XML uploade dans un bucket S3 ou consomme par un service comme Datadog Test Visibility donne des metriques précieuses : évolution de la durée par test, taux de flake par fichier, jours sans aucun changement. Ces metriques transforment des intuitions en décisions chiffrees. Le test le plus lent est-il vraiment indispensable ? Le module avec 30 % de flakes mérite-t-il une refonte ? Ces questions deviennent triviales avec les bonnes données.
La troisième pratique est la séparation des chemins critiques. Un workflow CI ne doit pas être tout-en-un. Un workflow par usage : un pour les tests sur PR (rapide), un pour le build de release (complet, plus lent), un pour les checks de sécurité (npm audit, snyk), un pour le déploiement. Cette structuré rend chaque workflow plus court et plus comprehensible.
Runners self-hosted : quand ca devient intéressant
GitHub Actions facture les minutes des runners cloud. Au-delà d’un certain volume (typiquement 5000 minutes par mois), il devient économique de provisionner ses propres runners self-hosted. Un VPS a 20 dollars par mois peut absorber 50 000 minutes de tests, ce qui représenté une économie significative.
Le coût cache est la maintenance : il faut gérer les mises a jour, la sécurité (les runners ont accès au repo et peuvent extraire des secrets), le cache disque qui se remplit. Pour une équipe de moins de cinq développeurs, rester sur les runners GitHub Actions cloud est plus simple et probablement moins cher en coût total. Au-delà de dix développeurs sur un projet actif, les runners self-hosted commencent a se justifier.
Stratégie de relance et flake automatique
Playwright supporte les retries automatiques via retries: 1 dans la config. Sur la CI, on active cette option pour absorber les flakes occasionnels — un test qui passe en deuxième tentative est marqué « flaky » en jaune dans le rapport, ce qui alerte sans bloquer le merge. La règle : on accepte un flake jaune temporaire, mais on cree systématiquement un ticket pour le traiter dans la semaine.
Au-delà de la simple relance, les outils d analyse de flakes (Trunk Flaky Tests, Datadog Test Visibility) detectent les patterns sur plusieurs runs : tel test échoue 8 % du temps les mardis matin, tel autre fail uniquement quand un autre test particulier passe avant. Cette intelligence statistique permet de cibler la maintenance la ou elle paie. Pour un projet sans budget pour ces outils, conserver le rapport JSON brut sur 30 jours et écrire un script ad-hoc qui le mouline donne déjà 80 % de la valeur.
Ressources officielles
Pour la vue d’ensemble stratégique sur les tests modernes, voir le guide principal : Tests modernes en JavaScript en 2026.
Questions fréquentes
Combien de shards activer ?
2 a 4 shards couvrent la plupart des projets. Au-delà, le temps d amorcage des runners (téléchargement, setup-node, npm ci) commence a dépasser le temps de tests. La règle simple : si chaque shard tourne moins de 60 secondes, c’est qu’il y en a trop. Réduire jusqu’à ce que chaque shard prenne 2 a 4 minutes.
GitHub Actions vs GitLab CI vs CircleCI ?
Les trois supportent le sharding et le cache. Les concepts sont identiques, seule la syntaxe YAML change. Pour un projet hébergé sur GitHub, Actions est le choix par défaut car intégré nativement.
Que faire si un shard échoue mais pas les autres ?
Vérifier d’abord que le test échoue de façon deterministe : relancer plusieurs fois. Si oui, debugger en local en passant --shard=2/4 pour reproduire. Si non, c’est un test flaky a traiter en priorité — ces tests minent la confiance dans toute la suite.
Puis-je sharder aussi Vitest ?
Oui, avec --shard=1/N depuis Vitest 0.34. Mais en general, Vitest tourne déjà en moins d’une minute, donc le sharding ajouté plus de complexité que de gain. A reserver aux suites unitaires de plus de 1000 fichiers.
Comment limiter le coût sur un compte payant ?
Filtrer les workflows par chemin (paths: ['src/**', 'tests/**']) pour ne pas tourner sur les changements de docs ou de README. Activer la matrice cross-OS uniquement sur main. Limiter rétention-days des artifacts a 7 jours pour ne pas accumuler du stockage.