Les données de test dans Playwright doivent être générées fraîchement à chaque exécution et isolées par test pour éviter les conflits en parallèle. Elles doivent aussi être nettoyées après chaque test pour maintenir la stabilité de l'environnement.

Le problème des données codées en dur

// Fragile — casse quand l'utilisateur est supprimé ou le mot de passe change
test('l\'utilisateur peut se connecter', async ({ page }) => {
  await page.fill('[data-testid="email"]', 'admin@test.com');
  await page.fill('[data-testid="password"]', 'AdminPass1');
});

Ce code dépend de données spécifiques existant dans la base de données. Des tests partageant un même compte provoquent des conflits en parallèle, et le test ne fonctionne que dans un environnement fixe sans changer l'email.

Schéma 1 : fichier de constantes

La première amélioration par rapport aux valeurs codées en dur : centraliser toutes les données de test dans un seul fichier.

// data/users.ts
export const TEST_USERS = {
  admin: {
    email: 'admin@test.com',
    password: 'AdminPass1',
    name: 'Test Admin',
    role: 'admin' as const,
  },
  member: {
    email: 'member@test.com',
    password: 'MemberPass1',
    name: 'Test Member',
    role: 'member' as const,
  },
  viewer: {
    email: 'viewer@test.com',
    password: 'ViewerPass1',
    name: 'Test Viewer',
    role: 'viewer' as const,
  },
} as const;

export const TEST_PRODUCTS = {
  basic: { id: 1, name: 'Basic Plan', price: 9.99 },
  pro: { id: 2, name: 'Pro Plan', price: 29.99 },
  enterprise: { id: 3, name: 'Enterprise', price: 99.99 },
} as const;

// Dans les tests
import { TEST_USERS, TEST_PRODUCTS } from '../data/users';

test('l\'admin peut accéder au tableau de bord', async ({ loginPage }) => {
  await loginPage.login(TEST_USERS.admin.email, TEST_USERS.admin.password);
});

Mieux que des valeurs dispersées, mais dépend toujours de l'existence de ces utilisateurs spécifiques.

Schéma 2 : fonctions factory

Les fonctions factory génèrent des données de test uniques par test :

// data/factories.ts
let counter = 0;

export function generateUser(overrides: Partial<User> = {}): CreateUserRequest {
  counter++;
  return {
    email: `test_user_${Date.now()}_${counter}@example.com`,
    password: 'ValidPass1!',
    name: `Test User ${counter}`,
    role: 'member',
    ...overrides,
  };
}

export function generateProduct(overrides: Partial<Product> = {}): CreateProductRequest {
  return {
    name: `Test Product ${Date.now()}`,
    price: Math.floor(Math.random() * 100) + 10,
    category: 'electronics',
    description: 'Un produit de test pour les tests automatisés',
    inStock: true,
    ...overrides,
  };
}

// Dans les tests
import { generateUser } from '../data/factories';

test('créer un nouvel utilisateur', async ({ request }) => {
  const userData = generateUser({ role: 'admin' });
  
  const response = await request.post('/api/users', {
    data: userData,
  });
  
  expect(response.status()).toBe(201);
  const created = await response.json();
  expect(created.email).toBe(userData.email);
});

Chaque test obtient un email unique. Plus de collisions.

Schéma 3 : configuration via API dans les fixtures

Créez des données fraîches via l'API avant chaque test, supprimez-les après :

// fixtures/index.ts
import { test as base } from '@playwright/test';
import { generateUser } from '../data/factories';

interface TestFixtures {
  testUser: { id: number; email: string; password: string; token: string };
  adminToken: string;
}

export const test = base.extend<TestFixtures>({
  // Token admin — partagé, portée worker (créé une fois par worker)
  adminToken: [async ({ request }, use) => {
    const response = await request.post('/api/auth/login', {
      data: { email: 'admin@test.com', password: 'AdminPass1' },
    });
    const { token } = await response.json();
    await use(token);
  }, { scope: 'worker' }],

  // Utilisateur de test — unique par test
  testUser: async ({ request, adminToken }, use) => {
    const userData = generateUser();
    
    // CRÉATION : nouvel utilisateur avant le test
    const createResp = await request.post('/api/users', {
      data: userData,
      headers: { Authorization: `Bearer ${adminToken}` },
    });
    const user = await createResp.json();

    // Connexion pour obtenir le token
    const loginResp = await request.post('/api/auth/login', {
      data: { email: userData.email, password: userData.password },
    });
    const { token } = await loginResp.json();

    // Donner au test ce dont il a besoin
    await use({ 
      id: user.id, 
      email: userData.email, 
      password: userData.password,
      token 
    });

    // TEARDOWN : supprimer après le test
    await request.delete(`/api/users/${user.id}`, {
      headers: { Authorization: `Bearer ${adminToken}` },
    });
  },
});

// tests/profile.spec.ts
import { test, expect } from '../fixtures';

test('l\'utilisateur peut mettre à jour son profil', async ({ page, testUser }) => {
  // testUser est un utilisateur frais, créé uniquement pour ce test
  await page.goto(`/login`);
  await page.fill('[data-testid="email"]', testUser.email);
  await page.fill('[data-testid="password"]', testUser.password);
  await page.click('[data-testid="submit"]');
  
  await page.click('[data-testid="edit-profile"]');
  await page.fill('[data-testid="name"]', 'Nom mis à jour');
  await page.click('[data-testid="save"]');
  
  await expect(page.getByTestId('profile-name')).toHaveText('Nom mis à jour');
  // Après le test : l'utilisateur est automatiquement supprimé
});

Schéma 4 : alimentation de la base de données

Pour les scénarios de données complexes, alimentez la base de données directement :

// setup/seed.ts
import { chromium } from '@playwright/test';

async function seed() {
  const response = await fetch('http://localhost:3000/api/seed', {
    method: 'POST',
    headers: { 
      'Content-Type': 'application/json',
      'X-Seed-Secret': process.env.SEED_SECRET || 'dev-seed-secret',
    },
    body: JSON.stringify({
      users: [
        { email: 'admin@test.com', password: 'AdminPass1', role: 'admin' },
        { email: 'member@test.com', password: 'MemberPass1', role: 'member' },
      ],
      products: [
        { name: 'Basic Plan', price: 9.99, category: 'subscription' },
        { name: 'Pro Plan', price: 29.99, category: 'subscription' },
      ],
    }),
  });
  
  if (!response.ok) {
    throw new Error(`Alimentation échouée : ${response.status}`);
  }
  
  console.log('Base de données alimentée avec succès');
}

seed();

Exécuter avant les tests : node setup/seed.ts && npx playwright test

Schéma 5 : état d'authentification sauvegardé

Évitez de vous connecter au début de chaque test. Connectez-vous une seule fois, sauvegardez l'état du navigateur, réutilisez-le :

// auth.setup.ts
import { test as setup } from '@playwright/test';
import path from 'path';

const authFile = path.join(__dirname, '../playwright/.auth/user.json');

setup('s\'authentifier', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="email"]', 'member@test.com');
  await page.fill('[data-testid="password"]', 'MemberPass1');
  await page.click('[data-testid="submit"]');
  await page.waitForURL('/dashboard');
  
  // Sauvegarder l'état de stockage (cookies, localStorage)
  await page.context().storageState({ path: authFile });
});

// playwright.config.ts
projects: [
  {
    name: 'setup',
    testMatch: /auth\.setup\.ts/,
  },
  {
    name: 'authenticated',
    use: {
      storageState: 'playwright/.auth/user.json',  // Déjà connecté
    },
    dependencies: ['setup'],
  },
],

Les tests du projet authenticated sautent le flux de connexion et démarrent directement avec une session déjà authentifiée.

Gérer les données pour différents environnements

Utilisez des variables d'environnement pour pointer vers les bonnes données :

// data/config.ts
export const ENV_USERS = {
  local: {
    admin: { email: 'admin@local.test', password: 'LocalAdmin1' },
  },
  staging: {
    admin: { email: 'admin@staging.test', password: process.env.STAGING_ADMIN_PASS! },
  },
  production: {
    // Utilisateur en lecture seule pour les smoke tests de production
    reader: { email: process.env.PROD_READER_EMAIL!, password: process.env.PROD_READER_PASS! },
  },
};

const env = (process.env.TEST_ENV || 'local') as keyof typeof ENV_USERS;
export const USERS = ENV_USERS[env];

TEST_ENV=staging npx playwright test --project=chromium

Gérer l'isolation des tests

Quand les tests s'exécutent en parallèle, ils ne doivent pas partager d'état mutable.

Mauvais — état partagé :

let userId: number;

test.beforeAll(async ({ request }) => {
  const user = await request.post('/api/users', { data: generateUser() });
  userId = (await user.json()).id;
});

// Plusieurs tests utilisent le même userId — race conditions !
test('voir l\'utilisateur', async ({ page }) => { await page.goto(`/users/${userId}`); });
test('modifier l\'utilisateur', async ({ page }) => { /* modifie le même utilisateur */ });
test('supprimer l\'utilisateur', async ({ page }) => { /* le supprime ! */ });

Bon — état isolé :

// Utiliser des fixtures pour que chaque test ait son propre utilisateur
test('voir l\'utilisateur', async ({ page, testUser }) => { 
  await page.goto(`/users/${testUser.id}`); 
});

test('modifier l\'utilisateur', async ({ page, testUser }) => { 
  // Ce testUser est différent de celui ci-dessus
});

Nettoyer après les tests

Nettoyez toujours les données que vos tests créent :

test('crée un produit', async ({ request, adminToken }) => {
  let productId: number;
  
  try {
    const response = await request.post('/api/products', {
      data: { name: 'Produit de test', price: 19.99 },
      headers: { Authorization: `Bearer ${adminToken}` },
    });
    const product = await response.json();
    productId = product.id;
    
    expect(response.status()).toBe(201);
    expect(product.name).toBe('Produit de test');
  } finally {
    // S'exécute toujours, même si le test échoue
    if (productId!) {
      await request.delete(`/api/products/${productId}`, {
        headers: { Authorization: `Bearer ${adminToken}` },
      });
    }
  }
});

Mieux : mettez le nettoyage dans le teardown de la fixture (après await use(...)) pour qu'il s'exécute toujours.

Récapitulatif

| Schéma | Idéal pour |

|--------|-----------|

| Fichier de constantes | Données de référence stables (rôles, catégories) |

| Fonctions factory | Générer des données de test uniques |

| Setup/teardown de fixture via API | Données fraîches et isolées par test |

| Alimentation de base de données | État initial complexe avant la suite de tests |

| État d'auth sauvegardé | Éviter la connexion dans chaque test |

L'approche la plus robuste combine ces schémas : des fonctions factory pour générer des données uniques, une fixture API pour créer et supprimer les données par test. Commencez par ces fixtures dès le premier test, pas quand vous avez déjà 50 tests avec des données codées en dur à refactorer.

→ See also: Données de Test Réutilisables: Factories, Fixtures et Faker.js dans Playwright | Isolation des Tests: Pourquoi Chaque Test Playwright Doit Être sans État | Gérer l'Authentification dans Playwright avec storageState (Sans Se Connecter à Chaque Test) | Tests d'API avec l'APIRequestContext de Playwright (Sans Postman)