Les bonnes pratiques d'automatisation des tests couvrent les conventions de nommage, les patterns d'assertion, la stratégie de locateurs, l'isolation des tests et la structure du Page Object Model. Ce sont les décisions qui déterminent si une suite de tests reste maintenable à 50 tests ou s'effondre à 200.

Nommez les tests comme des phrases, pas comme du code

Le nom d'un test est la première chose que vous lisez quand quelque chose échoue à 2h du matin en CI. Il doit vous dire exactement ce qui a cassé sans ouvrir le fichier.

Mauvais :

test('test de connexion', async ({ page }) => { ... });
test('test1', async ({ page }) => { ... });
test('vérifierTableau', async ({ page }) => { ... });

Bon :

test('l\'utilisateur peut se connecter avec des identifiants valides', async ({ page }) => { ... });
test('la connexion échoue avec un mot de passe incorrect', async ({ page }) => { ... });
test('le tableau des voyages affiche 5 lignes après connexion', async ({ page }) => { ... });

Le pattern est : [qui] peut/ne peut pas [faire quoi] [dans quelle condition]. Rédigez-le pour qu'une personne non technique lisant la sortie CI comprenne ce qui a échoué.

Les blocs describe fonctionnent de la même façon :

test.describe('Connexion', () => {
  test('réussit avec des identifiants valides', async ({ page }) => { ... });
  test('échoue avec un mauvais mot de passe', async ({ page }) => { ... });
  test('échoue avec un email vide', async ({ page }) => { ... });
});

Une assertion par test : l'idéal, pas la règle

Vous verrez des conseils disant "une assertion par test". La vraie règle est : un concept logique par test.

Un test qui se connecte et vérifie le titre de la page convient (ce sont des parties du même flux). Un test qui se connecte, vérifie le titre, modifie un enregistrement, vérifie la mise à jour, puis se déconnecte fait trop de choses. Quand il échoue, vous ne saurez pas quelle partie a cassé.

Bon. Un concept, plusieurs assertions liées :

test('la connexion redirige vers le tableau de bord avec le bon en-tête', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.getByLabel('Username').fill('admin@becomeqa.com');
  await page.getByLabel('Password').fill('testpass123');
  await page.getByRole('button', { name: 'Submit' }).click();

  await expect(page).toHaveURL(/dashboard/);
  await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
  await expect(page.getByText('My Travel Items')).toBeVisible();
});

Gardez les tests indépendants les uns des autres

Les tests qui dépendent les uns des autres sont un piège. Si le test 3 ne fonctionne que quand le test 2 a été exécuté avant, vous ne pouvez pas lancer les tests en parallèle. Vous ne pouvez pas lancer un seul test en isolation. Et quand le test 2 casse, vous obtenez une cascade d'échecs difficile à diagnostiquer.

Chaque test doit configurer son propre état et nettoyer après lui.

Chaque test nécessitant un utilisateur connecté doit faire la connexion lui-même, ou utiliser storageState pour sauvegarder le cookie d'authentification et le réutiliser sans répéter le flux UI.

// Mauvais : dépend que le test précédent ait fait la connexion
test('peut voir les voyages', async ({ page }) => {
  // suppose qu'on est déjà connecté — casse si lancé seul
  await expect(page.getByText('My Travel Items')).toBeVisible();
});

// Bon : configure son propre état
test('peut voir les voyages', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.getByLabel('Username').fill('admin@becomeqa.com');
  await page.getByLabel('Password').fill('testpass123');
  await page.getByRole('button', { name: 'Submit' }).click();
  await expect(page.getByText('My Travel Items')).toBeVisible();
});

N'utilisez jamais test.only dans du code commité. Ça désactive silencieusement tous les autres tests du fichier. Si un test.only est mergé, votre CI passe avec 1 test sur 50 et personne ne s'en rend compte jusqu'à ce que quelque chose casse en production.

Utilisez le Page Object Model quand les fichiers grossissent

Quand votre fichier de test dépasse 200 lignes et que chaque test répète les mêmes appels getByLabel('Username').fill(...), c'est le moment pour le Page Object Model (POM).

Le POM déplace les interactions avec la page dans une classe séparée. Les tests appellent des méthodes sur cette classe plutôt que des commandes Playwright brutes. Quand le formulaire de connexion change, vous mettez à jour une classe au lieu de chaque test qui touche à la connexion.

// pages/LoginPage.ts
import { Page } from '@playwright/test';

export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('https://lab.becomeqa.com');
    await this.page.getByRole('button', { name: 'Login' }).click();
  }

  async login(username: string, password: string) {
    await this.page.getByLabel('Username').fill(username);
    await this.page.getByLabel('Password').fill(password);
    await this.page.getByRole('button', { name: 'Submit' }).click();
  }
}

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('l\'utilisateur peut se connecter', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('admin@becomeqa.com', 'testpass123');

  await expect(page.getByText('My Travel Items')).toBeVisible();
});

Quand l'UI de connexion change, vous corrigez LoginPage.ts et tous les tests restent verts.

Ne vous précipitez pas vers le POM. Écrivez d'abord les tests sans. Quand vous vous surprenez à copier-coller les mêmes 5 lignes pour la troisième fois, c'est le signal.

Évitez les attentes codées en dur

page.waitForTimeout(3000) est un code smell. Vous dites à Playwright d'attendre 3 secondes peu importe ce qui est à l'écran. Le test devient lent sur les machines rapides et reste instable sur les runners CI lents.

Playwright attend automatiquement les éléments avant d'interagir avec eux. Quand vous devez vraiment attendre quelque chose de précis, attendez cette chose précise :

// Mauvais
await page.waitForTimeout(3000);
await page.getByRole('button', { name: 'Enregistrer' }).click();

// Bon — Playwright attend automatiquement avant de cliquer
await page.getByRole('button', { name: 'Enregistrer' }).click();

// Bon — attendre qu'un élément spécifique apparaisse
await page.waitForSelector('[data-testid="success-toast"]');

// Bon — attendre la fin d'une requête réseau
await page.waitForResponse(resp => resp.url().includes('/api/items'));

La seule fois où waitForTimeout est acceptable, c'est en débogage local pour ralentir l'exécution et voir ce qui se passe. Il ne doit jamais exister dans du code de test commité.

Utilisez des variables d'environnement pour les identifiants et les URLs

Coder en dur les identifiants et les URLs de base dans les tests crée deux problèmes. Ils s'infiltrent dans l'historique git, et les modifier signifie chercher dans chaque fichier de test.

Stockez-les dans un fichier .env et chargez-les via la configuration Playwright :

// .env (ne jamais commiter ce fichier)
BASE_URL=https://lab.becomeqa.com
TEST_USER=admin@becomeqa.com
TEST_PASS=testpass123

// playwright.config.ts
export default defineConfig({
  use: {
    baseURL: process.env.BASE_URL || 'https://lab.becomeqa.com',
  },
});

// tests/login.spec.ts
test('l\'utilisateur peut se connecter', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.getByLabel('Username').fill(process.env.TEST_USER!);
  await page.getByLabel('Password').fill(process.env.TEST_PASS!);
  await page.getByRole('button', { name: 'Submit' }).click();

  await expect(page.getByText('My Travel Items')).toBeVisible();
});

Ajoutez .env à .gitignore. En CI, définissez les variables d'environnement dans la configuration du pipeline à la place.

Structurez votre dossier de tests avant qu'il ne grossisse

Une structure de dossiers qui fonctionne pour 10 tests s'effondre à 100. Mettez-la en place tôt :

tests/
  auth/
    login.spec.ts
    logout.spec.ts
  items/
    items-list.spec.ts
    items-crud.spec.ts
  api/
    items-api.spec.ts
pages/
  LoginPage.ts
  ItemsPage.ts
fixtures/
  auth.fixture.ts

Regroupez par fonctionnalité, pas par type. tests/auth/ est mieux que tests/ui/ parce que quand quelque chose casse dans l'authentification, vous savez exactement où chercher.

Utilisez npx playwright test tests/auth/ pour lancer uniquement les tests d'un dossier. Pratique quand vous travaillez sur une fonctionnalité spécifique et ne voulez pas attendre toute la suite.

Écrivez des tests qui documentent l'intention

Un test est de la documentation. Les noms de variables doivent décrire ce qu'ils contiennent, les données de test doivent sembler réalistes, et les commentaires ne doivent apparaître que pour une configuration non évidente.

// Difficile à comprendre
const u = 'admin@becomeqa.com';
const p = 'testpass123';
await page.getByLabel('Username').fill(u);

// Clair
const adminEmail = 'admin@becomeqa.com';
const adminPassword = 'testpass123';
await page.getByLabel('Username').fill(adminEmail);

Petite différence dans le code, grande différence en lisibilité six mois plus tard.

Lancez la suite complète avant de merger

Les tests qui ne s'exécutent qu'en local sont des suggestions, pas des tests. Connectez vos tests Playwright à la CI pour qu'ils s'exécutent automatiquement sur chaque pull request.

Au minimum, votre CI doit installer les dépendances, installer les navigateurs, lancer la suite, et publier le rapport HTML si les tests échouent :

# .github/workflows/tests.yml (simplifié)
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test

Si les tests passent en CI, vous mergez. S'ils échouent, vous corrigez avant de merger. C'est le contrat.

FAQ

Combien de tests est trop pour un fichier ?

Environ 300 à 400 lignes, quand faire défiler pour trouver un test devient pénible. Divisez par fonctionnalité à ce stade.

Dois-je tester chaque cas limite ?

Non. Testez le chemin nominal, le chemin d'erreur le plus courant, et les cas limites qui ont causé de vrais bugs. L'objectif est la confiance, pas 100% de couverture pour elle-même.

Mes tests passent en local mais échouent en CI. Qu'est-ce qui est généralement en cause ?

Trois causes les plus courantes : une URL localhost codée en dur, un await manquant, ou une condition de course masquée par votre machine plus rapide que le runner CI. Vérifiez la sortie du trace viewer depuis la CI, elle montre exactement où ça a cassé.

Quand utiliser beforeEach vs une fixture ? beforeEach pour une configuration simple spécifique à un fichier de test. Les fixtures pour une configuration réutilisée dans plusieurs fichiers (comme une page connectée ou des données de test pré-chargées). → See also: Page Object Model dans Playwright: Du Chaos à la Maintenabilité | Fixtures Playwright Expliquées: Des Intégrées aux Personnalisées | Déboguer les Tests Instables: Un Guide Pratique | Isolation des Tests: Pourquoi Chaque Test Playwright Doit Être sans État