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.
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 :
// 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é :
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
Aucun commentaire pour le moment.