L'importance des tests front-end dans les applications modernes
Les composants React constituent l'interface directe entre vos utilisateurs et votre application. Un comportement inattendu sur un formulaire, une latence sur un bouton ou une regression sur un flux critique peuvent avoir un impact immédiat sur la rétention utilisateur. Chez Dub.co, enfrastructure open-source de gestion de liens, les tests automatisés ont permis de réduire significativement les régressions en production tout en accélérant les cycles de développement.
Les équipes ont observé une baisse notable des incidents signalés en production, une revue de code plus efficace et un délai de livraison des fonctionnalités raccourci. Pour un projet utilisé par des milliers de développeurs, cette rigueur est indispensable.
Architecture de test adoptée par Dub.co
Le projet s'appuie sur une approche en couches combinant Vitest comme runner et React Testing Libray comme bibliothèque principale. Les tests sont organisés par module fonctionnel dans une arborescence claire :
tests/
├── statistiques/ # composants liés à l'analytique
├── raccourcis/ # gestion des liens courts
├── metadonnees/ # parsing des balises OG
├── redirigés/ # logiques de redirection
├── etiquettes/ # système de tags
├── utilitaires/ # fonctions utilitaires de test
└── espaces-travail/ # gestion des workspaces
Configuration Vitest
Le fichier de configuration se trouve à la racine de l'application web et définit l'environnement d'exécution :
import { defineConfig } from "vitest/config";
import { loadEnv } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
root: "./tests",
reporters: ["verbose"],
globals: true,
testTimeout: 50000,
env: loadEnv("", process.cwd(), ""),
},
});
Quelques points notables : l'activation de globals évite d'importer manuellement les fonctions de test, le délai d'attente prolongé assure la stabilité des tests asynchrones, et le plugin tsconfigPaths permet d'utiliser directement les alias définis dans TypeScript.
Principes fondamentaux avec React Testing Library
L'équipe applique une règle stricte : tester ce que l'utilisateur perçoit, pas les détails d'implémentation. Plutôt que de vérifier qu'un état interne a changé, les assertions portent sur le rendu visible et les comportements observables.
Exemple concret : création d'un lien court
Voici un test simulant la création d'un lien avec une clé personnalisée :
test("création d'un lien avec clé personnalisée", async () => {
const customKey = generateUniqueSlug();
const response = await httpClient.post<ShortLink>({
endpoint: "/links",
params: { workspaceId },
payload: {
destinationUrl: targetUrl,
slug: customKey,
domain: baseUrl,
},
});
expect(response.status).toBe(200);
expect(response.data).toMatchObject({
...linkFixture,
slug: customKey,
destinationUrl: targetUrl,
ownerId: currentUser.id,
workspaceId,
fullUrl: `https://${baseUrl}/${customKey}`,
qrCodeUrl: `https://api.dub.co/qr?url=https://${baseUrl}/${customKey}?qr=1`,
assignedTags: [],
});
expect(LinkSchema.safeParse(response.data).success).toBe(true);
await testHarness.removeLink(response.data.id);
});
Ce test couvre quatre étapes essentielles : génération de données aléatoires pour l'isolation, simulation de l'appel API via un client HTTP, validation multi-niveau (statut, structure, conformité au schéma), puis nettoyage des données de test.
Outils utilitaires mutualisés
Pour limiter la duplication, plusieurs helpers sont mis à disposition :
- IntegrationHarness : initialise l'environnement et gère le cycle de vie des ressources
- factories.ts : produit des jeux de données standardisés
- schemas.ts : définit les contrats de données attendsu
- utils.ts : fonctions auxiliaires communes
Couverture et intégration continue
Des seuils de couverture sont définis par catégorie de code : les composants métier critiques visent 90%, les fonctions utilitaires 95%, et les composants d'interface 85%. La commande pnpm test:coverage génère un rapport détaillé permettant d'identifier les zones non couvertes.
Chaque pull request déclenche automatiquement la suite de tests dans le pipeline CI/CD. Aucun merge n'est autorisé tant qu'un test échoue, garantissant que seule du code validé atteint la production.
Scénarios avancés
Redirection par appareil
La fonctionnalité de redirection ciblée par type de device nécessite une couverture spécifique :
test("redirection selon le système d'exploitation", async () => {
const iosTarget = "https://apps.apple.com/app/1611158928";
const androidTarget = "https://play.google.com/store/apps/details?id=com.disney.disneyplus";
const { status, data: createdLink } = await httpClient.post<ShortLink>({
endpoint: "/links",
params: { workspaceId },
payload: {
destinationUrl: targetUrl,
domain: baseUrl,
iosTarget,
androidTarget,
},
});
expect(status).toBe(200);
expect(createdLink).toMatchObject({
...linkFixture,
destinationUrl: targetUrl,
iosTarget,
androidTarget,
ownerId: currentUser.id,
workspaceId,
fullUrl: `https://${baseUrl}/${createdLink.slug}`,
qrCodeUrl: `https://api.dub.co/qr?url=https://${baseUrl}/${createdLink.slug}?qr=1`,
assignedTags: [],
});
});
Redirection géographique
test("redirection selon la zone géographique", async () => {
const geoRouting = {
AF: `${targetUrl}/AF`,
AL: `${targetUrl}/AL`,
DZ: `${targetUrl}/DZ`,
};
const { status, data: createdLink } = await httpClient.post<ShortLink>({
endpoint: "/links",
params: { workspaceId },
payload: {
destinationUrl: targetUrl,
domain: baseUrl,
geoRouting,
},
});
expect(status).toBe(200);
expect(createdLink.geoRouting).toEqual(geoRouting);
});
Recommandations issues de la pratique
Privilégier les comportements observables
Plutôt que d'affirmer qu'un état interne a été modifié, on vérifie le rendu perçu par l'utilisateur. Par exemple, on préfère expect(screen.getByText('Lien créé')).toBeInTheDocument() à une assertion sur un appel de fonction interne.
Utiliser des schémas de données réalistes
Les tests s'appuient sur les mêmes contrats de validation qu'en production, via LinkSchema, ce qui permet de détecter dès la phase de test les incohérences structurelles.
Concevoir des suites maintenables
- Chaque test doit être autonome et exécutable indépendamment
- Les noms de tests doivent exprimer clairement l'intention
- Les assertions regroupées logiquement au sein d'un même cas
- Les ressources créées doivent systématiquement être nettoyées
Adopter une approche incrémentale
Il est plus efficace de commencer par couvrir les parcours critiques, puis d'étendre progressivement vers les cas limites et les conditions aux bornes, plutôt que de viser immédiatement une couverture totale.