Playwright exécute les fichiers de test en parallèle par défaut, un fichier par worker, et peut être configuré pour exécuter chaque test individuellement en parallèle avec le mode fullyParallel.
Ce que Playwright fait par défaut
Avant de changer quoi que ce soit, il est utile de comprendre ce que Playwright fait sans configuration.
Par défaut, Playwright exécute les fichiers de test en parallèle et les tests d'un même fichier de façon séquentielle. Chaque processus worker prend un fichier de test, exécute tous les tests de ce fichier du haut vers le bas, puis prend le fichier suivant disponible. Plusieurs workers tournent simultanément, chacun traitant un fichier différent.
Le nombre de workers par défaut est la moitié du nombre de CPU logiques de la machine. Sur un laptop de développeur typique avec 8 cœurs, vous obtenez 4 workers. Sur un runner CI avec 2 cœurs, vous en avez 1, ce qui signifie que les tests s'exécutent séquentiellement sauf si vous le surchargez.
// playwright.config.ts — comportement par défaut (aucune modification nécessaire)
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// workers vaut par défaut la moitié des CPU logiques
// les tests dans un fichier s'exécutent séquentiellement par défaut
});Ce comportement par défaut est un bon point de départ. Les tests d'un même fichier partagent souvent un état de configuration de façon implicite (même page object, même flux de connexion, mêmes fixtures de données). L'exécution séquentielle dans un fichier les protège. Les fichiers séparés tournent simultanément, ce qui accélère l'exécution sans exiger une isolation parfaite entre chaque test.
Configurer les workers dans playwright.config.ts
L'option workers contrôle le nombre de processus parallèles qui exécutent vos tests. Vous pouvez la définir comme un nombre absolu ou comme un pourcentage des CPU disponibles.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Nombre absolu — toujours utiliser exactement 4 workers
workers: 4,
// Ou comme pourcentage des CPU disponibles
// workers: '75%',
// Ou des valeurs différentes selon l'environnement
// workers: process.env.CI ? 2 : '50%',
});La forme en pourcentage est utile quand vous voulez que la même config fonctionne sur des machines différentes. '50%' sur une machine à 8 cœurs donne 4 workers ; sur un runner CI à 2 cœurs, ça donne 1. Vous dites à Playwright "utilise la moitié de la machine" plutôt que de coder un nombre en dur.
Vous pouvez aussi surcharger les workers depuis la ligne de commande sans toucher à la config :
# Lancer avec un nombre spécifique de workers
npx playwright test --workers=4
# Forcer l'exécution séquentielle (1 worker)
npx playwright test --workers=1--workers=1 est utile pour déboguer des problèmes d'isolation. Si les tests passent avec 1 worker mais échouent avec 4, vous avez un problème d'état partagé quelque part.
--workers=1. Si le test passe de façon constante, vous avez affaire à une race condition ou à un état partagé entre tests, pas à un bug dans le test lui-même.Mode fullyParallel : tout exécuter en même temps
Le mode standard exécute les fichiers en parallèle mais les tests dans un fichier séquentiellement. fullyParallel: true supprime cette restriction. Chaque test individuel s'exécute en parallèle quel que soit son fichier.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true, // Tous les tests s'exécutent en parallèle
workers: 4,
});Cela peut réduire drastiquement le temps d'exécution pour les grandes suites. Une suite de 100 tests répartis sur 10 fichiers (si chaque test prend 2 secondes) passe de 20 secondes à environ 5 secondes avec 4 workers en mode fullyParallel.
Le revers : fullyParallel exige que chaque test soit complètement isolé. Pas de contexte de navigateur partagé, pas d'état de connexion partagé qui se modifie, pas de tests qui supposent s'exécuter dans un ordre précis. Si deux tests tentent de modifier la même ligne d'une base de données partagée simultanément, vous aurez des échecs intermittents difficiles à reproduire.
Avant d'activer fullyParallel, auditez votre suite de tests. Vérifiez les tests qui créent des données avec des IDs codés en dur, qui supposent qu'un test précédent a déjà tourné, ou qui ne réinitialisent pas l'état de la page.
Si vos tests utilisent test.beforeEach pour se connecter à nouveau et travaillent avec des données uniques, fullyParallel est sans risque. Si vous partagez un contexte de navigateur pré-authentifié stocké dans une variable de module, ils ne sont pas prêts pour ça.
test.describe.serial() pour les tests intentionnellement séquentiels
Parfois, un groupe de tests doit vraiment s'exécuter dans l'ordre. Un flux de checkout où le test 1 ajoute un article au panier, le test 2 applique un coupon, et le test 3 finalise l'achat : ces tests sont intrinsèquement séquentiels. test.describe.serial() est l'outil approprié.
import { test, expect } from '@playwright/test';
test.describe.serial('flux de checkout', () => {
test('ajoute un article au panier', async ({ page }) => {
await page.goto('/products/widget-pro');
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
test('applique un code promo', async ({ page }) => {
await page.goto('/cart');
await page.getByPlaceholder('Coupon code').fill('SAVE10');
await page.getByRole('button', { name: 'Apply' }).click();
await expect(page.getByTestId('discount-amount')).toBeVisible();
});
test('finalise l\'achat', async ({ page }) => {
await page.goto('/checkout');
await page.getByLabel('Card number').fill('4242424242424242');
await page.getByLabel('Expiry').fill('12/26');
await page.getByLabel('CVC').fill('123');
await page.getByRole('button', { name: 'Pay now' }).click();
await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
});
});Avec test.describe.serial(), Playwright exécute ces trois tests dans l'ordre et s'arrête si l'un d'eux échoue. Exécuter "finalise l'achat" quand "ajoute un article au panier" a échoué n'a aucun sens.
Utilisez serial avec parcimonie. Chaque bloc serial est une section de votre suite qui ne peut pas être parallélisée. Si vous vous retrouvez à ajouter serial à la plupart des blocs describe, le vrai remède est de rendre vos tests indépendants les uns des autres. Générez des données de test uniques, utilisez des contextes de navigateur isolés, nettoyez après chaque test.
Isolation des tests : prérequis pour l'exécution parallèle
L'exécution parallèle amplifie les problèmes d'isolation. Un test qui fonctionne bien seul échouera de façon imprévisible quand il tourne en même temps qu'un autre test qui touche les mêmes données ou le même état.
Le principe fondamental : chaque test doit posséder ses données et ne pas dépendre de ce qu'un autre test a laissé.
import { test, expect } from '@playwright/test';
// MAUVAIS : état partagé entre les tests
let userId: number;
test('crée un utilisateur', async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'Alice', email: 'alice@example.com' }
});
userId = (await response.json()).id; // variable partagée — race condition en attente
});
test('met à jour l\'utilisateur', async ({ request }) => {
// Si le test précédent n'a pas encore tourné (ou a tourné dans un autre worker), userId est undefined
await request.put(`/api/users/${userId}`, {
data: { name: 'Alice Updated' }
});
});import { test, expect } from '@playwright/test';
// BON : chaque test crée et possède ses propres données
test('met à jour un utilisateur', async ({ request }) => {
// Créer l'utilisateur dans ce test
const createResponse = await request.post('/api/users', {
data: {
name: 'Alice',
email: `alice-${Date.now()}@example.com` // email unique pour éviter les conflits
}
});
const { id } = await createResponse.json();
// Maintenant le mettre à jour — nous possédons cet utilisateur
const updateResponse = await request.put(`/api/users/${id}`, {
data: { name: 'Alice Updated' }
});
expect(updateResponse.status()).toBe(200);
});Pour les tests UI, la fixture page de Playwright donne à chaque test son propre contexte de navigateur par défaut. Cette partie est gérée pour vous. Les problèmes d'isolation viennent généralement des données de test dans une base de données partagée, pas de l'état du navigateur.
test.beforeAll pour créer des données partagées et test.afterAll pour les nettoyer semble efficace, mais crée des dépendances cachées entre les tests. Si un test modifie les données partagées, les tests suivants cassent. Préférez test.beforeEach avec des données par test, même si c'est plus lent.Sharding : répartir votre suite sur plusieurs machines CI
Les workers parallélisent les tests sur une machine. Le sharding répartit la suite de tests sur plusieurs machines. Ces deux mécanismes sont indépendants et complémentaires. Vous pouvez utiliser les deux ensemble.
Le flag --shard prend un argument current/total :
# Exécuter le shard 1 sur 3 (premier tiers des tests)
npx playwright test --shard=1/3
# Exécuter le shard 2 sur 3
npx playwright test --shard=2/3
# Exécuter le shard 3 sur 3
npx playwright test --shard=3/3Playwright répartit les fichiers de test uniformément entre les shards. Avec 30 fichiers de test et 3 shards, chaque shard reçoit 10 fichiers. La distribution est déterministe. Vous obtiendrez les mêmes fichiers dans le même shard à chaque exécution.
Vous pouvez combiner le sharding avec les workers. Chaque shard tourne avec plusieurs workers, ce qui vous donne du parallélisme à la fois au sein du shard et entre les shards :
# Chaque shard utilise 4 workers en interne
npx playwright test --shard=1/3 --workers=4Le sharding est surtout utile en CI, où vous pouvez provisionner plusieurs machines pour une même exécution de pipeline.
Matrix GitHub Actions pour le sharding parallèle
GitHub Actions supporte les builds en matrix, exécutant un job plusieurs fois avec des entrées différentes. Combiné au sharding Playwright, c'est ainsi qu'on répartit une suite lente sur des machines parallèles.
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run tests (shard ${{ matrix.shard }}/4)
run: npx playwright test --shard=${{ matrix.shard }}/4
env:
BASE_URL: ${{ vars.BASE_URL }}
TEST_USER: ${{ secrets.TEST_USER }}
TEST_PASS: ${{ secrets.TEST_PASS }}
- name: Upload shard report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-shard-${{ matrix.shard }}
path: playwright-report/
retention-days: 7fail-fast: false est important ici. Par défaut, si un job de la matrix échoue, GitHub annule les jobs restants. Avec fail-fast: false, tous les shards s'exécutent jusqu'à la fin même si l'un échoue. Vous obtenez le tableau complet de ce qui a passé et ce qui a échoué dans toute la suite.
L'argument install chromium uniquement sur l'étape d'installation du navigateur fait gagner du temps. Si vous exécutez des tests multi-navigateurs, changez ça en --with-deps sans spécifier de navigateur pour installer les trois.
Pour fusionner les rapports de shards en un seul rapport, ajoutez un job de fusion après la fin de la matrix :
merge-reports:
needs: test
runs-on: ubuntu-latest
if: always()
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Download all shard reports
uses: actions/download-artifact@v4
with:
pattern: playwright-report-shard-*
path: all-reports/
merge-multiple: false
- name: Merge reports
run: npx playwright merge-reports --reporter html ./all-reports/*/
- name: Upload merged report
uses: actions/upload-artifact@v4
with:
name: playwright-report-merged
path: playwright-report/
retention-days: 14Vous obtenez un rapport HTML téléchargeable couvrant tous les shards, avec tous les résultats de tests en un seul endroit.
Mesurer l'accélération et trouver le bon nombre de workers
Plus de workers ne signifie pas toujours des tests plus rapides. Augmenter les workers accroît la contention des ressources : plus de CPU, plus de mémoire, plus de processus de navigateur en compétition sur la même machine. À un moment donné, ajouter un worker ralentit les choses parce que la machine est surchargée.
La règle approximative : workers = nombre de CPU logiques fonctionne bien pour les charges CPU intensives. Pour les tests de navigateur, qui attendent surtout le réseau et le rendu, vous pouvez souvent aller plus haut. Workers = 2x CPU est une expérience raisonnable.
Comment mesurer :
# Référence : 1 worker (séquentiel)
npx playwright test --workers=1 2>&1 | grep "passed\|failed\|Duration"
# Essayer 2 workers
npx playwright test --workers=2 2>&1 | grep "passed\|failed\|Duration"
# Essayer 4 workers
npx playwright test --workers=4 2>&1 | grep "passed\|failed\|Duration"
# Essayer 8 workers
npx playwright test --workers=8 2>&1 | grep "passed\|failed\|Duration"Tracez les résultats. Vous cherchez le point d'inflexion où ajouter des workers cesse de réduire le temps. C'est votre nombre optimal pour cette machine.
Pour la CI spécifiquement, vérifiez les ressources que votre runner fournit. Les runners ubuntu-latest de GitHub Actions ont 4 vCPU et 16 Go de RAM. Avec des tests de navigateur Playwright, 4 workers est un bon point de départ. Vous pourriez gagner 5 à 10% avec plus, mais vous verrez de la pression mémoire à partir de 8+ workers sur ce runner.
Une formule pratique pour calculer le bénéfice du sharding :
Temps avec N shards ≈ (temps total de la suite sur 1 machine) / N + overhead fixe par shard
Overhead fixe = checkout + npm ci + installation navigateur ≈ 60-90 secondesSi votre suite prend 10 minutes avec 1 machine, 4 shards l'amènent à environ 2,5 minutes + 90 secondes d'overhead = ~4 minutes. C'est un vrai gain. Si votre suite prend 3 minutes, 4 shards l'amènent à 45 secondes + 90 secondes = 2,5 minutes. Ça ne vaut pas la complexité ajoutée.
Le seuil du sharding : commencez à l'envisager quand votre suite prend régulièrement plus de 5 minutes sur une seule machine CI.
// playwright.config.ts — config parallèle prête pour la production
import { defineConfig } from '@playwright/test';
const isCI = !!process.env.CI;
export default defineConfig({
testDir: './tests',
// Parallélisme complet — nécessite des tests isolés
fullyParallel: true,
// Calibrer les workers selon l'environnement
workers: isCI ? 4 : '50%',
// Relances uniquement en CI — ne pas masquer les échecs en local
retries: isCI ? 1 : 0,
// Timeout par test
timeout: 30_000,
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
},
});Cette config est claire sur les arbitrages : parallélisme complet partout, workers fixes en CI et pourcentage en local. Les relances sont réservées à la CI pour ne pas masquer les problèmes pendant le développement.
FAQ
Mes tests passent en local mais échouent en parallèle. Par où commencer ?Lancez avec --workers=1 et confirmez que les tests passent. Essayez ensuite --workers=2. Si ça échoue, vous avez un problème d'état partagé entre exactement deux tests qui tournent maintenant simultanément. Vérifiez les variables de module, les lignes de base de données partagées avec des IDs codés en dur, ou tout état qui persiste entre les tests. La solution est presque toujours de déplacer la configuration dans beforeEach et d'utiliser des identifiants uniques pour les données de test.
Playwright trie les fichiers de test par ordre alphabétique et les distribue en round-robin entre les shards. Vous ne contrôlez pas l'attribution directement. Si un shard prend systématiquement beaucoup plus de temps que les autres (tous les tests lents dans le même shard), c'est un problème de distribution. Divisez les grands fichiers de test en fichiers plus petits pour que les shards reçoivent des charges plus équilibrées.
Puis-je exécuter des tags ou patterns grep spécifiques par shard au lieu d'utiliser--shard ?
Oui, et certaines équipes préfèrent ça pour la prévisibilité. Par exemple, --grep @checkout sur une machine et --grep @catalog sur une autre. L'inconvénient : la maintenance manuelle. Vous devez mettre à jour les patterns grep à mesure que vous ajoutez des tests. --shard est automatique et sans maintenance.
fullyParallel: true affecte-t-il l'ordre d'apparition des résultats dans le rapport ?
Oui. Avec fullyParallel, les résultats apparaissent au fur et à mesure que les tests se terminent, pas dans l'ordre des fichiers. Le rapport HTML regroupe toujours par fichier et test, donc la lisibilité n'est pas affectée. La sortie terminal semble juste plus entrelacée.
workers dans la config et --shard en ligne de commande ?
workers contrôle le parallélisme dans un processus sur une machine. --shard divise la suite sur plusieurs invocations, typiquement sur des machines différentes. Ils opèrent à des niveaux différents et fonctionnent ensemble. Chaque shard peut avoir plusieurs workers.
→ See also: Déboguer les Tests Instables: Un Guide Pratique | CI/CD pour QA: GitHub Actions, Jenkins et GitLab Comparés | GitHub Actions pour Tests Playwright: La Configuration Complète (2026) | Isolation des Tests: Pourquoi Chaque Test Playwright Doit Être sans État | Fichier de Configuration Playwright Expliqué: Toutes les Options à Connaître