Aller au contenu
Me contacter
Tous les articles
6 min de lecture

Quelle stratégie de tests pour un projet React ou Next.js ?

Que tester, avec quel outil et dans quelle proportion : la logique pure avec Vitest, les composants avec React Testing Library, quelques parcours critiques avec Playwright, ce qu’il vaut mieux ne pas tester, et comment faire tourner le tout en CI.

  • Tests
  • React
  • Next.js
  • Vitest
  • React Testing Library
  • Playwright
  • TDD

Sur un projet React ou Next.js, la question n’est pas de savoir s’il faut tester, mais quoi tester, avec quel outil et jusqu’où. Trop peu de tests, et chaque modification devient risquée ; trop de tests mal ciblés, et chaque refactoring casse des dizaines de tests sans qu’aucun comportement ait changé. Voici une stratégie pragmatique, outil par outil, avec les particularités de Next.js et de ses Server Components, qui couvre ce qui compte sans alourdir le projet.

À quoi servent les tests ?

Un test automatisé a d’abord une fonction : signaler une régression avant qu’elle n’atteigne la production. Il permet aussi de refactoriser en confiance, puisqu’un comportement qui change fait échouer un test, et il documente le code, chaque test décrivant un cas d’usage précis.

Le taux de couverture, en revanche, est un mauvais objectif. Il mesure les lignes exécutées et non les comportements vérifiés, et pousse à écrire des tests sans assertion utile pour atteindre un chiffre. La bonne question à se poser devant un test est plutôt de savoir quelle régression réelle il permettrait de détecter.

Quels types de tests, et dans quelle proportion ?

Trois niveaux se complètent, chacun avec son coût et le degré de confiance qu’il apporte :

  • les tests unitaires vérifient une fonction isolée, en quelques millisecondes ;
  • les tests de composants vérifient qu’un composant réagit correctement aux actions de l’utilisateur, dans un DOM simulé ;
  • les tests de bout en bout pilotent un vrai navigateur sur l’application complète, plus lents mais au plus près de l’usage réel.

Une répartition raisonnable place beaucoup de tests unitaires sur la logique, des tests de composants sur les éléments interactifs et seulement quelques tests de bout en bout sur les parcours qui ne doivent jamais casser. Plus un test monte en niveau, plus il rassure, mais plus il est lent et fragile.

Tester la logique pure avec Vitest

Le calcul d’un prix, la validation d’un formulaire, un tri ou un formatage de date n’ont aucune raison de vivre dans un composant. Extraits dans des fonctions pures, ils se testent sans rendu, sans navigateur et sans simulation, ce qui donne les tests les plus rapides et les plus stables. Vitest s’intègre naturellement aux projets Vite et Next.js, et reprend l’API de Jest :

import {describe, expect, it} from 'vitest';
import {cartTotal} from './cart';

describe('cartTotal', () => {
  it('sums the lines and applies the discount', () => {
    const lines = [{price: 2000, qty: 2}, {price: 500, qty: 1}];
    expect(cartTotal(lines, 0.1)).toBe(4050); // 4 500 - 10 %
  });

  it('returns 0 for an empty cart', () => {
    expect(cartTotal([], 0)).toBe(0);
  });
});

Les montants sont exprimés en centimes, pour éviter les erreurs d’arrondi des nombres à virgule. Un bon test unitaire porte un nom qui décrit un comportement, vérifie une seule chose et couvre les cas limites : liste vide, valeur nulle, date invalide.

Tester les composants avec React Testing Library

React Testing Library repose sur un principe simple : plus un test ressemble à la façon dont le logiciel est utilisé, plus il inspire confiance. On y retrouve donc les éléments comme le ferait un utilisateur, par leur rôle et leur libellé, et on interagit avec eux par des clics et des saisies. L’état interne du composant, ses props ou ses classes CSS n’apparaissent jamais dans le test.

import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {NewsletterForm} from './NewsletterForm';

it('shows an error when the email is invalid', async () => {
  const user = userEvent.setup();
  render(<NewsletterForm />);

  await user.type(screen.getByLabelText('E-mail'), 'not-an-email');
  await user.click(screen.getByRole('button', {name: 'S’inscrire'}));

  expect(screen.getByRole('alert')).toHaveTextContent('Adresse e-mail invalide');
});

Ce test survit à un changement de bibliothèque de formulaires ou à une réécriture complète du composant, tant que le comportement reste le même. Il vérifie aussi, en passant, que le champ a un libellé et que l’erreur est annoncée, deux points d’accessibilité. Les matchers comme toHaveTextContent viennent du paquet @testing-library/jest-dom, et les appels réseau se simulent au niveau de la requête avec MSW plutôt qu’en remplaçant les modules du composant.

Avec Next.js, une limite est à connaître. Vitest et React Testing Library testent les Client Components et les Server Components synchrones, mais pas les Server Components asynchrones, qui chargent leurs données avec await. La documentation de Next.js recommande de les couvrir par des tests de bout en bout. En pratique, la logique qu’ils contiennent, comme celle des Server Actions, s’extrait dans des fonctions pures testées unitairement, et le composant ne fait plus qu’assembler.

Couvrir les parcours critiques avec Playwright

Certains parcours ne doivent jamais casser, comme la connexion, la commande, le paiement ou le formulaire de contact. Un test de bout en bout les rejoue dans un vrai navigateur, sur l’application complète, avec son routage, ses appels serveur et son rendu réel. Playwright les écrit avec les mêmes sélecteurs par rôle et par libellé que React Testing Library :

import {expect, test} from '@playwright/test';

test('a visitor can send the contact form', async ({page}) => {
  await page.goto('/contact');
  await page.getByLabel('E-mail').fill('visitor@example.com');
  await page.getByLabel('Message').fill('Bonjour');
  await page.getByRole('button', {name: 'Envoyer'}).click();

  await expect(page.getByRole('status')).toContainText('Message envoyé');
});

Ces tests coûtent cher à écrire et à exécuter, et en garder un petit nombre bien choisi vaut mieux que d’en accumuler. Les lancer sur le build de production (next build puis next start, que Playwright démarre lui-même grâce à son option webServer), et non sur le serveur de développement, permet de détecter aussi les erreurs propres à ce build. Playwright peut enfin intégrer un audit d’accessibilité automatique avec axe.

Que vaut-il mieux ne pas tester ?

  • Un composant purement visuel, sans comportement, que la revue de code et un coup d’œil à l’écran vérifient mieux qu’un test.
  • Les détails d’implémentation (état interne, nom d’une fonction, structure du DOM) : un test qui casse à chaque refactoring sans qu’aucun comportement ait changé teste le code, et non le produit.
  • Les bibliothèques tierces, déjà testées par leurs auteurs ; seule leur intégration dans l’application mérite un test.
  • De grands snapshots de composants, que l’on finit par mettre à jour sans les lire et qui ne protègent alors plus de rien.

Pourquoi écrire le test d’abord ?

Le TDD (Test-Driven Development) inverse l’ordre habituel. On écrit le test, on le voit échouer, on écrit le code minimal qui le fait passer, puis on améliore le code sans casser le test. Voir le test échouer d’abord prouve qu’il vérifie bien quelque chose ; un test écrit après coup passe souvent du premier coup, sans qu’on sache s’il aurait détecté une erreur.

La même règle s’applique aux bugs : avant de corriger, on écrit le test qui reproduit le problème. Il échoue, la correction le fait passer, et le bug ne peut plus revenir sans être détecté. Cette discipline s’applique aussi au code produit avec un assistant IA, comme le décrit le guide Claude Code au quotidien.

Faire tourner les tests en CI

Des tests qui ne tournent que sur le poste du développeur finissent par être oubliés. La CI les exécute à chaque pull request, dans un ordre qui fait échouer vite : le typage et le lint, puis les tests unitaires et de composants, puis le build, et enfin les tests de bout en bout sur ce build. Une pull request dont les tests échouent ne doit pas pouvoir être fusionnée.

Un test instable, qui échoue une fois sur dix sans raison apparente, se corrige ou se supprime sans attendre. Le relancer jusqu’à ce qu’il passe apprend à l’équipe à ignorer les échecs, et le filet perd alors toute sa valeur.

Une bonne stratégie de tests tient en quelques principes : beaucoup de tests rapides sur la logique pure, des tests de composants qui raisonnent comme l’utilisateur, quelques parcours critiques de bout en bout et une CI qui ne laisse rien passer. L’objectif n’est pas un chiffre de couverture, mais la confiance de pouvoir modifier le code sans craindre la production. Pour en discuter, écrivez-moi.