DEV Community

Jérémy Chauvin
Jérémy Chauvin

Posted on

2

Tester son interface efficacement : des patterns pour des tests d’UI robustes

Article co-écrit avec @manon_carbonnel

Tester l’interface (UI) est un défi : les tests doivent être fiables, lisibles et maintenables dans le temps, tout en restant suffisamment souples pour suivre les évolutions du produit. Malheureusement, l’UI est amenée à évoluer régulièrement, et ces tests sont souvent fragiles et difficiles à faire évoluer si leur conception n’est pas rigoureuse. Nous voulons également des tests cohérents avec des parcours réalistes.

Cet article explore des patterns éprouvés pour améliorer la qualité des tests d’UI. Ces tests sont aussi l’occasion de se mettre à la place des utilisateur·ices de nos applications.

User Facing Attributes

Ce pattern propose d’utiliser des sélecteurs basés sur des attributs ou propriétés visibles par l’utilisateur·ice (rôles accessibles, labels, textes, etc.) pour identifier et interagir avec les éléments de l'interface. Cela permet de rendre les tests plus intuitifs et orientés vers l'expérience réelle de l'utilisateur·ice, en étant moins dépendants de CSS, tout en favorisant des pratiques de développement prenant en compte l’accessibilité.

💡 C’est aujourd’hui l’approche préconisée par la majorité des outils d’automatisation de navigateur comme Playwright.
Si vous utilisez Cypress, bien que la documentation évoque Testing Library, elle préconise l’utilisation de data-test-id.
Mais nous ne sommes pas d’accord avec cette recommandation :D.
Dans la même idée, utiliser des locators basés sur le CSS ou le XPath ne sont pas recommandés, car le DOM peut souvent changer, ce qui conduit à des tests non résilients.

Exemples de code pour accéder aux éléments du DOM d’une page web

Exemple d’utilisation avec Testing Library

// tests/homepage.spec.ts
import { describe, it, expect } from 'vitest';
import { screen } from '@testing-library/dom';

describe('HomePage - User Facing Attributes', () => {
  it('devrait afficher un titre contenant "Playwright" et un lien "Get started" visible', () => {
    await this.page.goto('https://playwright.dev/');

    // Sélection basée sur des attributs exposés à l'utilisateur·ice
    const header = screen.getByRole('heading', { level: 1, name: /Playwright/i });
    expect(header).toBeInTheDocument();

    const getStartedLink = screen.getByRole('link', { name: /Get started/i });
    expect(getStartedLink).toBeVisible();
  });
});

Enter fullscreen mode Exit fullscreen mode

Exemple d’utilisation avec Playwright

// tests/homepage.spec.ts
import { test, expect, Page, Locator } from '@playwright/test';
import { PlaywrightHomePage } from '../pages/PlaywrightHomePage';

test.describe('Test du site Playwright via User Facing Attributes', () => {
  test('Vérifier que le titre principal contient "Playwright" et que le lien "Get started" est visible', async ({ page }) => {
    await this.page.goto('https://playwright.dev/');

    // Sélection basée sur des attributs exposés à l'utilisateur·ice
    const header = this.page.getByRole('heading', { level: 1 });
    const getStartedLink = this.page.getByRole('link', { name: /Get started/i });

    await expect(header).toContainText('Playwright');
    await expect(getStartedLink).toBeVisible();
  });
});

Enter fullscreen mode Exit fullscreen mode

Avantages

  • Robustesse des tests :

    Les tests sont moins susceptibles de casser lors de modifications techniques (comme des changements d'attributs CSS ou ordre des balises dans le DOM).

  • Alignement avec l'UX :

    Les sélecteurs utilisés correspondent à ce que voit et utilise réellement l'utilisateur·ice, améliorant ainsi la pertinence des tests.

  • Promotion des bonnes pratiques d'accessibilité :

    Toutes les interfaces ne sont pas toujours dotées d'attributs ou de rôles pertinents, ce pattern incite donc développer des interfaces accessibles et bien structurées.

  • Lisibilité et clarté :

    Les tests sont plus explicites, car ils décrivent les éléments de l'interface tels qu'une personne les perçoit.

Inconvénients

  • Dépendance au contenu textuel :

    Les tests peuvent devenir fragiles si les libellés ou textes changent fréquemment, même si la fonctionnalité reste correcte.

  • Moins de contrôle sur des sélecteurs complexes :

    Pour certaines interactions spécifiques ou des éléments très dynamiques (comme un élément dans un tableau ou une liste de données), il peut être nécessaire d'utiliser des sélecteurs techniques en complément ou remplacement.

Page Object Model

Son objectif principal est de séparer la logique de test (les scénarios et assertions) de la logique d'interaction avec l'interface.

Exemples de code avec Playwright

// pages/PlaywrightHomePage.ts
import { Page, Locator } from '@playwright/test';

export class PlaywrightHomePage {
  readonly page: Page;
  readonly header: Locator;

  constructor(page: Page) {
    this.page = page;
    // On cible ici le titre principal de la page (par exemple, l'élément h1)
    this.header = page.locator('h1');
  }

  // Méthode pour naviguer vers la page d'accueil
  async navigate(): Promise<void> {
    await this.page.goto('https://playwright.dev/');
  }

  // Méthode pour récupérer le texte du titre
  async getHeaderText(): Promise<string | null> {
    return this.header.textContent();
  }
}
Enter fullscreen mode Exit fullscreen mode

Exemple d’utilisation

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

test.describe('Test du site Playwright', () => {
  test('Vérifier que le titre principal contient "Playwright"', async ({ page }) => {
    // Instanciation de la page via notre Page Object
    const homePage = new PlaywrightHomePage(page);
    await homePage.navigate();

    const headerText = await homePage.getHeaderText();

    expect(headerText).toContain('Playwright');
  });
});

Enter fullscreen mode Exit fullscreen mode

Avantages

  • Séparation des responsabilités :

    Le POM sépare la logique de test de l’implémentation de l’interface. Cela permet aux tests de se concentrer sur la validation du comportement métier.

  • Modularité et évolutivité :

    Chaque page ou composant est représenté par une classe spécifique, facilitant ainsi l’organisation et l’évolution de la suite de tests.

  • Maintenance facilitée :

    Lorsqu'une modification est apportée à l'interface (changement de sélecteur, structure HTML modifiée…), il suffit de mettre à jour la classe correspondante, sans toucher aux tests eux-mêmes.

  • Réutilisabilité :

    Les méthodes et sélecteurs encapsulés dans un objet peuvent être réutilisés dans plusieurs tests, réduisant ainsi la duplication de code.

  • Lisibilité et clarté des tests :

    En déléguant les interactions à des objets dédiés, les scénarios de tests restent concis et lisibles. On peut ainsi comprendre rapidement l’intention du test sans se perdre dans les détails d’implémentation.

Inconvénients

  • Risque de duplication si mal structuré :

    Sans une bonne conception, on peut se retrouver avec des classes qui dupliquent des comportements similaires pour des composants récurrents, au lieu d’extraire des composants communs. Dans le cas où on trouverait plusieurs fois le même composant sur une page, il suffit d’y accéder en précisant le bloc parent dans le sélecteur.

  • Couplage si contenu dans une librairie à part :

    Conserver le POM dans un repository / une librairie à part entraîne des problèmes d’alignement des sélecteurs lors de mise à jour des pages.

Screenplay Pattern : alternative au POM

Le Screenplay Pattern est une approche pour structurer les tests d'UI en mettant l'accent sur les actions et les intentions des utilisateur·ices, plutôt que sur la structure technique des pages. Particulièrement utile pour des suites de tests larges et évolutives

Au lieu de manipuler directement les éléments de l’UI via un Page Object Model (POM), il introduit des "acteur·ices" qui interagissent avec l'interface en utilisant des "tâches" et des "questions".

Exemples de code avec Playwright

import { Page } from '@playwright/test';

// Définition des intéractions
export class EnterText {
  static into(selector: string, text: string) {
    return async (page: Page) => {
      await page.fill(selector, text);
    };
  }
}

export class Click {
  static on(selector: string) {
    return async (page: Page) => {
      await page.click(selector);
    };
  }
}

// Définition des tâches
export class Login {
  static withCredentials(username: string, password: string) {
    return async (page: Page) => {
      await EnterText.into('#username', username)(page);
      await EnterText.into('#password', password)(page);
      await Click.on('#login-button')(page);
    };
  }
}

// Définition des questions
export class IsLoggedIn {
  static async answeredBy(page: Page) {
    return await page.locator('#welcome-message').isVisible();
  }
}
Enter fullscreen mode Exit fullscreen mode

Exemple d’utilisation

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { Login, IsLoggedIn } from './login-screenplay';

test('Alice peut se connecter', async ({ page }) => {
  await page.goto('https://example.com/login');

  // L'utilisateur·ice joue le rôle d'un·e acteur·ice réalisant une tâche
  await Login.withCredentials('alice', 'securepassword')(page);

  // Vérification de la connexion
  const loggedIn = await IsLoggedIn.answeredBy(page);
  expect(loggedIn).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

Avantages

  • Modularité :

    Les interactions et tâches sont réutilisables.

  • Lisibilité :

    Le code ressemble plus à un scénario utilisateur.

  • Maintenance facilitée :

    Modifier l’implémentation d’une interaction ne casse pas toute la suite de tests.

  • Scalabilité :

    Facilite l’ajout de nouvelles tâches sans dupliquer du code.

Inconvénients

  • Complexité initiale :

    Plus de fichiers et d'abstraction qu'un simple Page Object Model qui est parfois suffisant pour de petits tests simple.

  • Courbe d’apprentissage :

    Nécessite de bien comprendre la séparation entre acteurs, tâches et interactions.

Outils et bibliothèques populaires pour des test d’UI

Testing Library

Testing library est une solution légère pour tester des UI en interagissant avec le DOM de manière similaire à une personne. Son approche repose sur la recherche et l'interaction avec les éléments via leurs user-facing attributes.

Playwright

Playwright est un outil d'automatisation de navigateurs conçu pour offrir une developer experience confortable et puissante. Il prend en charge plusieurs langages (TypeScript, JavaScript, Python, C#, Java) et permet de tester des applications sur différents navigateurs (Chromium, Firefox, WebKit) et plateformes (Windows, Linux, macOS). Son API facilite la manipulation du DOM et intègre nativement les user-facing attributes pour interagir avec les éléments. Playwright se distingue également par ses fonctionnalités avancées comme le record & replay, les tests en parallèle, et le debugging interactif avec des outils visuels comme le trace viewer.

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay