Retour à l'accueil

Playwright : DI et isolation pour 1000 tests

L'article décrit l'architecture pour exécuter plus de 1000 tests Playwright : getters dans POM pour éviter les pièges d'état, DI via fixtures avec mergeTests, isolation des données avec semis RUN_ID de Faker et factories. Convient aux développeurs middle/senior.

1000 tests parallèles en Playwright sans échecs
Advertisement 728x90

Passer à l'échelle avec Playwright : Des objets page à l'injection de dépendances et l'isolation des données

Dans les applications monopages (SPA) avec rendu dynamique, les constructeurs d'objets page présentent des risques dus à l'asynchronicité. Tenter d'initialiser un état dans le constructeur via une IIFE entraîne des conditions de concurrence : le test accède aux données avant qu'elles ne soient prêtes, ce qui se manifeste en CI lors d'exécutions parallèles.

// Incorrect : état dans le constructeur
constructor(page: Page) {
  (async () => {
    this.initialCount = await page.locator('.items').count();
  })();
}

L'initialisation synchrone standard des localisateurs est également sous-optimale — l'objet capture un instantané du DOM au moment de sa création. Les accesseurs (getters) résolvent le problème avec une liaison tardive :

// Correct : accesseurs pour le DOM actuel
get submitButton() {
  return this.page.getByRole('button', { name: 'Passer la commande' });
}

Cela garantit un comportement sans état : chaque appel voit le DOM actuel, sans être lié à l'état initial.

Google AdInline article slot

Injection de dépendances via les fixtures Playwright

La création directe d'objets dans les tests (new LoginPage(page)) complique le refactoring : modifier le constructeur nécessite des modifications dans tout le projet. Les fixtures agissent comme un conteneur d'injection de dépendances avec mise en cache des instances.

export const test = base.extend({
  cartPage: async ({ page }, use) => {
    await use(new CartPage(page));
  },
  checkoutFlow: async ({ cartPage, checkoutPage }, use) => {
    await use(new CheckoutFlow(cartPage, checkoutPage));
  }
});

test('passer commande', async ({ authFlow, checkoutFlow }) => {
  await authFlow.loginAs(user);
  await checkoutFlow.submitOrder();
});

Les tests se concentrent sur la spécification, indépendamment de l'implémentation.

Passer à l'échelle des fixtures : mergeTests et espaces de noms

Avec 20+ fixtures, un fichier monolithique devient ingérable. Divisez par domaine avec mergeTests :

Google AdInline article slot
// auth.fixtures.ts
export const authTest = base.extend({ ... });

// fixtures.ts
import { mergeTests } from '@playwright/test';
export const test = mergeTests(authTest, cartTest);

Évitez les conflits de noms via les espaces de noms :

export const test = base.extend({
  auth: async ({ page }, use) => {
    await use({ admin: new Admin(page), user: new User(page) });
  }
});

test('L\'administrateur peut se connecter', async ({ auth }) => {
  await auth.admin.loginAs();
});

Isolation des données dans les exécutions parallèles

workerIndex est insuffisant pour la CI avec des fragments (shards). Utilisez une combinaison :

  • RUN_ID de la CI (ex : GITHUB_RUN_ID)
  • testId — hachage du chemin du test
  • repeatEachIndex pour les nouvelles tentatives

L'initialisation (seeding) de Faker avec cette clé composite garantit la reproductibilité :

Google AdInline article slot
export function seedFaker(testInfo: TestInfo) {
  const RUN_ID = process.env.RUN_ID || 'local';
  const seed = hashCode(`${testInfo.testId}-${RUN_ID}`);
  faker.seed(seed);
  return faker;
}

export const test = base.extend({
  faker: async ({}, use, testInfo) => {
    await use(seedFaker(testInfo));
  }
});

Les exécutions locales avec CI-RUN_ID reproduisent exactement les données des échecs.

Couches de données : Usines, Surcharges, Jeux de données

  • Faker : bruit pour les champs non critiques (noms, email)
  • Usines (Factories) : structure avec des valeurs par défaut
  • Surcharges (Overrides) : modifications critiques dans les tests
  • Jeux de données (Datasets) : cas métier récurrents
export function createUser(overrides?: Partial<User>, f = faker): User {
  return {
    id: f.string.uuid(),
    name: f.person.fullName(),
    role: 'client',
    ...overrides
  };
}

export const VIP_USER = { role: 'vip', discount: 0.15 };

Test : const vipUser = createUser({ ...VIP_USER }, faker);

Sémantique des étapes et gardes ESLint

Séparez les clics techniques de la logique métier avec @Step :

@Step('Passer la commande #${orderId}')
async submitOrder(orderId: string) {
  await this.checkoutPage.fillDetails(orderId);
  await this.checkoutPage.submit();
}

Les étapes métier ('Connexion') sont plus résilientes aux changements d'interface que les étapes techniques ('Cliquer sur le bouton Connexion').

ESLint interdit le new PageObject direct :

{
  "no-restricted-syntax": ["error", {
    "selector": "NewExpression[callee.name=/.*Page$/]",
    "message": "Utilisez des fixtures au lieu de new"
  }]
}

Points clés à retenir

  • Les accesseurs dans le modèle objet page garantissent l'accès au DOM actuel sans pièges d'état
  • Les fixtures comme injection de dépendances minimisent le refactoring avec 1000+ tests
  • RUN_ID + testId initialise Faker pour la stabilité dans les fragments CI
  • Usines + jeux de données isolent les données sans code redondant
  • Les espaces de noms des fixtures évitent les collisions dans mergeTests

— Editorial Team

Advertisement 728x90

Lire ensuite