Blog
Tester l’architecture, les défaillances et les parcours utilisateur avec Next.js
Des tests Next.js fiables dépassent les snapshots de composants et les réponses API. Une stratégie de production doit protéger les frontières architecturales, les défaillances, les comportements serveur et client et les parcours utilisateur complets.
Un test de composant réussi ne prouve pas que l’application fonctionne
Une application Next.js peut compter des centaines de tests unitaires réussis et pourtant échouer au cours du parcours qui importe réellement au client.
Un bouton peut s’afficher correctement alors que :
- le serveur renvoie des données dont la structure est inattendue,
- la session expire pendant l’envoi,
- une route lève une exception pendant le rendu,
- des données mises en cache restent obsolètes après une mutation,
- une requête est envoyée deux fois,
- une redirection perd la destination d’origine,
- une error boundary masque les actions de récupération utiles,
- une API disponible uniquement dans le navigateur échoue pendant le rendu serveur,
- un état de chargement ne se termine jamais,
- une mise à jour optimiste affiche des données qui n’ont pas été enregistrées.
La stratégie de test doit donc protéger davantage que les composants.
Une application Next.js de production doit être testée selon trois dimensions liées :
Architecture
→ Les responsabilités et le sens des dépendances sont-ils corrects ?
Défaillances
→ L’application se comporte-t-elle de manière sûre lorsque ses dépendances échouent ?
Parcours utilisateur
→ L’utilisateur peut-il atteindre un objectif important au moyen de l’interface réelle ?Tester ces dimensions séparément est utile.
Tester leurs interactions est ce qui protège véritablement l’application.
Commencer par le comportement dont dépend l’entreprise
Les équipes commencent souvent par demander :
Quel pourcentage de couverture de tests devons-nous atteindre ?Une meilleure question est :
Quelles défaillances empêcheraient l’utilisateur d’accomplir une tâche importante ?Prenons un parcours de commande en ligne :
Ouvrir un produit
→ ajouter l’article au panier
→ ouvrir le passage de commande
→ saisir les informations de livraison
→ envoyer la commande
→ autoriser le paiement
→ afficher la confirmationL’entreprise ne se soucie pas de savoir si chaque composant React possède un snapshot isolé.
Elle veut savoir si :
- le bon produit arrive dans le panier,
- les prix ne peuvent pas être modifiés depuis le navigateur,
- la commande n’est pas envoyée deux fois,
- un échec de paiement est récupérable,
- une session expirée ne fait pas perdre toutes les données saisies,
- un paiement réussi produit une seule commande,
- la confirmation affiche la bonne référence de commande.
Le parcours doit déterminer la stratégie de test.
Une cartographie utile des tests Next.js
Une architecture de test pragmatique peut se présenter ainsi :
Vérifications statiques
│
├── TypeScript
├── linting
└── règles d’import et d’architecture
│
▼
Tests du domaine et de l’application
│
├── calculs métier
├── transitions d’état
└── orchestration des cas d’usage
│
▼
Tests de composants
│
├── interaction
├── validation
├── chargement
└── erreurs récupérables
│
▼
Tests de contrat et d’intégration
│
├── réponses du backend
├── authentification
├── route handlers
└── invalidation du cache
│
▼
Tests de parcours dans le navigateur
│
├── navigation
├── formulaires complets
├── redirections
└── parcours critiques
│
▼
Vérification en production
├── logs
├── métriques
├── traces
└── contrôles synthétiquesChaque niveau répond à une question différente.
N’utiliser que des tests navigateur rend les échecs longs à diagnostiquer.
N’utiliser que des tests unitaires laisse les défauts d’intégration sans protection.
Un pipeline CI doit exécuter des contrôles rapides et déterministes à chaque changement, isoler les données des tests navigateur et traiter les parcours interservices plus lents comme des garde-fous de livraison fondés sur le risque, non comme un diagnostic facultatif.
Tester l’architecture avant qu’elle ne devienne difficile à modifier
Les tests d’architecture vérifient où le code est autorisé à dépendre.
Un projet Next.js maintenable peut employer des frontières semblables à celles-ci :
app
→ composition des routes et intégration au framework
features
→ cas d’usage visibles par l’utilisateur
domain
→ types et règles métier
application
→ orchestration et ports
infrastructure
→ clients API, stockage et adaptateurs de fournisseurs
ui
→ composants de présentation réutilisablesLes noms exacts des dossiers comptent moins que le sens des dépendances.
Une fonctionnalité ne devrait pas obliger un composant React à connaître :
- les structures brutes de la base de données,
- les objets de réponse du SDK d’un fournisseur,
- les détails internes des cookies d’authentification,
- les codes d’erreur d’un prestataire de paiement,
- les détails d’implémentation du cache.
Une règle conceptuelle de dépendance est :
UI
→ contrat applicatif
→ contrat du domaine
← adaptateur d’infrastructureL’infrastructure implémente le contrat.
La couche application ne dépend pas du fournisseur.
Exemple : isoler la dépendance du passage de commande
Au lieu d’appeler directement une API externe depuis un composant :
export function CheckoutForm(): JSX.Element {
async function submit(): Promise {
await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(formValues),
});
}
return {/* ... */};
}définissez un contrat appartenant au projet :
export type CheckoutInput = Readonly;
export type CheckoutResult =
| Readonly
| Readonly
| Readonly;
export interface CheckoutGateway {
submit(input: CheckoutInput): Promise;
}Un adaptateur d’infrastructure possède les détails HTTP :
export class HttpCheckoutGateway implements CheckoutGateway {
public constructor(
private readonly baseUrl: string,
private readonly fetcher: typeof fetch,
) {}
public async submit(
input: CheckoutInput,
): Promise {
const response = await this.fetcher(
`${this.baseUrl}/orders`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': input.idempotencyKey,
},
body: JSON.stringify(input),
},
);
if (!response.ok) {
throw await mapCheckoutFailure(response);
}
return validateCheckoutResult(await response.json());
}
}Les tests peuvent désormais remplacer la gateway sans remplacer globalement le réseau du navigateur.
L’adaptateur peut disposer de tests de contrat ciblés.
Le composant peut disposer de tests de comportement.
Le parcours complet peut toujours être couvert par un test navigateur.
Les tests d’architecture doivent imposer de vraies frontières
Selon les outils déjà présents dans le projet, les règles d’architecture peuvent être vérifiées au moyen :
- de restrictions d’import du linter,
- d’une analyse des dépendances,
- des références de projets TypeScript,
- des frontières entre packages,
- de tests ciblés du code source.
Parmi les règles utiles :
Le code du domaine ne doit pas importer React ou Next.js.
Les cas d’usage applicatifs ne doivent pas importer les SDK des fournisseurs.
Les composants UI ne doivent pas importer directement les implémentations d’infrastructure.
Un module fonctionnel ne doit pas accéder aux fichiers internes d’un autre module.
Les modules réservés au serveur ne doivent pas être importés dans les bundles client.
Les fichiers de route doivent composer les dépendances plutôt que contenir des parcours métier complets.Ne créez pas de règles uniquement parce qu’elles semblent élégantes.
Chaque règle doit empêcher une catégorie réelle de défaut.
Tester le comportement observable, pas l’implémentation interne
Un test de composant fragile affirme :
Le composant a appelé setState trois fois.Un test utile affirme :
Le bouton d’envoi devient indisponible pendant l’exécution de la requête.
Une requête en échec affiche une erreur récupérable.
Les valeurs saisies par l’utilisateur restent présentes.
Un résultat réussi mène à la page de confirmation.Les tests doivent survivre aux refactorisations internes lorsque le comportement visible par l’utilisateur ne change pas.
Évitez d’affirmer :
- les appels de helpers privés,
- les détails d’implémentation des hooks,
- la profondeur exacte de l’arbre de composants,
- des snapshots HTML complets pour des écrans complexes,
- les noms de classes générés par le framework.
Affirmez les résultats et les rôles d’accessibilité stables.
Tester séparément les responsabilités serveur et client
Les applications Next.js combinent fréquemment des données rendues côté serveur et des interactions côté client.
Une séparation utile est :
Responsabilité du serveur
→ authentification
→ chargement initial des données
→ autorisation
→ composition stable de la page
Responsabilité du client
→ interaction immédiate
→ validation locale
→ état en attente
→ nouvelle tentative
→ mises à jour progressivesLe test serveur doit vérifier que les utilisateurs non autorisés ne reçoivent pas les données protégées.
Le test de composant doit vérifier que l’utilisateur voit un état utile lorsqu’une mutation côté client échoue.
Le test navigateur doit vérifier le comportement complet.
Ne forcez pas chaque condition à passer par un test navigateur coûteux.
Les cas de défaillance doivent faire partie de la conception
La plupart des tests commencent avec des réponses réussies :
gateway.submit.mockResolvedValue({
status: 'completed',
orderId: 'order-123',
confirmationNumber: 'ORD-2026-123',
});Les incidents de production commencent généralement ailleurs :
L’API a dépassé le délai d’attente.
L’utilisateur a cliqué deux fois.
La réponse était un JSON valide, mais sa structure était invalide.
La session a expiré.
Le panier a changé.
Le prestataire de paiement a accepté le paiement, mais la redirection a échoué.
Le cache affichait d’anciennes données.Un parcours doit disposer d’une matrice explicite des défaillances.
Exemple de matrice des défaillances du passage de commande
Défaillance Comportement attendu de l’application
──────────────────────────────────────────────────────────────────────────────
La requête produit échoue Afficher une nouvelle tentative sans faire planter la route
Le panier est vide Rediriger ou afficher un état vide explicite
Le panier a changé côté serveur Actualiser le panier et demander confirmation
La session a expiré Demander la connexion et conserver le chemin de retour
La requête expire Afficher un état inconnu ; ne pas renvoyer aveuglément
Le paiement est refusé Conserver le contexte et permettre un autre moyen
Schéma de réponse invalide Afficher un échec sûr ; signaler la violation du contrat
Envoi en double Réutiliser la clé d’idempotence et empêcher une double commande
La confirmation échoue Permettre la récupération grâce à la référence de commande
Le panier en cache est obsolète Invalider ou actualiser après la mutation acceptéeChaque ligne importante doit être couverte au niveau de test fiable le moins coûteux.
Tester le chargement comme un état, pas comme un snapshot
Le comportement pendant le chargement doit répondre aux questions suivantes :
- Qu’est-ce qui reste utilisable ?
- Qu’est-ce qui est désactivé ?
- L’utilisateur peut-il quitter la page ?
- La mise en page reste-t-elle stable ?
- Que se passe-t-il après une réponse lente ?
- Existe-t-il un délai limite ou une voie de nouvelle tentative ?
- L’écran annonce-t-il la progression de manière accessible ?
Un test de composant peut vérifier :
it('prevents duplicate checkout submission while pending', async () => {
const deferred = createDeferred();
const submit = vi.fn(() => deferred.promise);
render();
await userEvent.click(
screen.getByRole('button', { name: /place order/i }),
);
expect(
screen.getByRole('button', { name: /place order/i }),
).toBeDisabled();
await userEvent.click(
screen.getByRole('button', { name: /place order/i }),
);
expect(submit).toHaveBeenCalledTimes(1);
deferred.resolve({
status: 'completed',
orderId: 'order-123',
confirmationNumber: 'ORD-123',
});
});Adaptez la syntaxe au test runner actuellement utilisé par le projet.
L’exemple utilise React Testing Library avec des mocks de style Vitest ; un projet utilisant Jest doit conserver les mêmes assertions comportementales plutôt que changer de stratégie uniquement à cause du runner.
L’affirmation importante est qu’une réponse lente ne provoque pas un envoi en double.
Tester explicitement les erreurs récupérables
Un message d’erreur ne suffit pas lorsque l’utilisateur ne peut pas reprendre son action.
Exemple :
it('keeps entered delivery data after a recoverable failure', async () => {
const submit = vi
.fn()
.mockRejectedValueOnce(new NetworkUnavailable())
.mockResolvedValueOnce({
status: 'completed',
orderId: 'order-123',
confirmationNumber: 'ORD-123',
});
render();
await userEvent.type(
screen.getByLabelText(/delivery note/i),
'Leave with reception',
);
await userEvent.click(
screen.getByRole('button', { name: /place order/i }),
);
expect(
await screen.findByRole('alert'),
).toHaveTextContent(/could not submit/i);
expect(
screen.getByLabelText(/delivery note/i),
).toHaveValue('Leave with reception');
await userEvent.click(
screen.getByRole('button', { name: /try again/i }),
);
expect(submit).toHaveBeenCalledTimes(2);
});Ce test protège l’effort fourni par l’utilisateur, pas seulement le message d’erreur.
Distinguer les refus métier des défaillances techniques
Ces résultats ne sont pas équivalents :
Prestataire de paiement indisponible
Le panier contient un article indisponible
L’utilisateur n’a pas l’autorisation
Le backend a renvoyé des données mal forméesUtilisez des types de résultat et des catégories d’erreur explicites.
Un refus métier peut être rendu comme un résultat normal du parcours :
if (result.status === 'rejected') {
return showCheckoutCorrection(result.reason);
}Une défaillance technique peut nécessiter :
- une nouvelle tentative,
- une journalisation,
- des métriques d’incident,
- un comportement de repli.
Ne transformez pas toutes les défaillances en un générique Something went wrong.
Les messages génériques peuvent convenir pour des raisons de sécurité, mais la classification interne doit rester précise.
Valider les réponses du backend à la frontière
Les types TypeScript ne valident pas le JSON à l’exécution.
Ce code n’est pas sûr :
const result = await response.json() as CheckoutResult;L’assertion ne fait qu’instruire le compilateur.
Elle ne vérifie pas la réponse.
Utilisez l’approche de validation à l’exécution déjà présente dans le projet :
const payload: unknown = await response.json();
return checkoutResultSchema.parse(payload);ou un validateur équivalent appartenant au projet.
Testez :
- les propriétés manquantes,
- les valeurs d’énumération incorrectes,
- les nombres incorrects,
- les valeurs nulles inattendues,
- les données supplémentaires non prises en charge lorsque cela compte,
- les versions incompatibles du backend.
Une violation du contrat doit échouer près de l’adaptateur, plutôt que provoquer ultérieurement une erreur sans rapport dans un composant.
Tester les défaillances au niveau des routes
Une route peut échouer avant que le composant interactif soit rendu.
Parmi les exemples :
- l’échec de la recherche d’authentification,
- un service backend indisponible,
- un paramètre de route invalide,
- un enregistrement absent,
- un refus d’autorisation,
- le dépassement du délai du chargeur de données,
- une structure de réponse inattendue.
Testez le comportement de la route avec l’approche déjà utilisée par le projet.
Vérifiez le résultat prévu :
Enregistrement absent
→ expérience not-found
Utilisateur non authentifié
→ connexion ou comportement d’accès refusé
Défaillance temporaire du backend
→ erreur de route récupérable
Paramètre de route invalide
→ réponse contrôlée, pas d’exception non géréeUtilisez les conventions actuelles du framework pour les erreurs, le chargement et les pages introuvables, telles qu’elles existent déjà dans le projet.
Ne créez pas une infrastructure d’erreur personnalisée si l’application possède déjà un modèle établi.
Les error boundaries doivent permettre la récupération
Une error boundary ne devrait pas se contenter d’afficher :
Something went wrong.Elle devrait répondre aux questions suivantes :
- Qu’est-ce qui a échoué ?
- La défaillance est-elle temporaire ?
- L’opération peut-elle être retentée ?
- La navigation reste-t-elle disponible ?
- Les données saisies par l’utilisateur ont-elles été conservées ?
- Une nouvelle tentative risque-t-elle de dupliquer l’action ?
- La défaillance est-elle signalée de manière sûre ?
Un test utile au niveau de la route vérifie :
La requête initiale lève une exception
→ l’interface d’erreur apparaît
→ l’utilisateur déclenche une nouvelle tentative
→ la seconde requête réussit
→ le contenu attendu de la page apparaîtN’affirmez pas les fonctions internes de réinitialisation du framework lorsque le même comportement peut être vérifié au travers de l’interface.
L’authentification doit être testée pendant le parcours
Tester uniquement la redirection initiale d’une page protégée ne suffit pas.
Les sessions peuvent expirer pendant que l’utilisateur :
- modifie un long formulaire,
- attend un paiement,
- téléverse un fichier,
- complète plusieurs étapes d’onboarding,
- conserve l’application ouverte dans un autre onglet.
Un test réaliste doit couvrir :
L’utilisateur ouvre le passage de commande en étant authentifié
→ saisit des données
→ la session expire
→ l’envoi renvoie unauthorized
→ l’utilisateur est invité à s’authentifier
→ l’application conserve une destination de retour sûre
→ le parcours peut reprendre sans double envoiNe conservez pas de secrets ni de corps de requête sensibles dans les URL de redirection.
La bonne stratégie de récupération dépend de la sensibilité des données.
Les tests d’autorisation appartiennent à la frontière serveur
Masquer un bouton ne constitue pas une autorisation.
Testez que les requêtes non autorisées sont rejetées même lorsqu’elles sont construites manuellement.
Par exemple :
Un utilisateur du tenant A demande une commande du tenant B
→ le serveur refuse l’accès
→ les données protégées ne figurent pas dans la réponse
→ l’événement est journalisé conformément à la politique du projetLe test navigateur peut vérifier le comportement visible.
Un test d’intégration à un niveau inférieur doit vérifier la véritable frontière d’autorisation.
Tester la navigation et les redirections
Les défauts de redirection apparaissent souvent lorsque :
- les préfixes de locale sont perdus,
- les paramètres de requête disparaissent,
- les URL de callback sont mal encodées,
- une redirection d’authentification boucle,
- une mutation réussie redirige vers des données obsolètes,
- l’historique du navigateur se comporte de manière inattendue.
Pour les parcours critiques, vérifiez :
- l’URL de destination,
- la locale conservée,
- le chemin de retour sûr conservé,
- le comportement du bouton Retour,
- l’accès direct à la route de confirmation,
- l’actualisation après le succès.
Ne testez pas chaque lien au moyen d’un navigateur.
Concentrez les tests navigateur sur les transitions du parcours.
Le comportement du cache exige des tests dédiés
Le cache peut donner l’impression qu’une mutation correcte est cassée.
Exemple :
L’utilisateur met à jour son adresse de livraison
→ le backend l’enregistre
→ l’application redirige vers la page de l’adresse
→ la page mise en cache affiche l’ancienne adresseUn test doit vérifier le contrat visible de l’extérieur :
Après une mutation acceptée, la prochaine lecture pertinente affiche la nouvelle valeur.Selon l’architecture du projet, cela peut nécessiter :
- l’invalidation du cache,
- l’invalidation de tags,
- la revalidation d’un chemin ou d’une route,
- le versionnement des clés de cache,
- le contournement du cache pour les données propres à un utilisateur.
Utilisez le mécanisme de cache réellement configuré dans le projet.
Ne testez pas les détails internes du framework.
Testez que des données obsolètes ne sont pas présentées là où la fraîcheur est requise.
Éviter les hypothèses globales sur le cache
Des données identiques peuvent avoir des exigences de fraîcheur différentes :
Description publique d’un produit
→ peut tolérer une obsolescence de courte durée
Stock disponible pendant le paiement
→ exige une validation serveur à jour
Préférence du profil utilisateur
→ doit normalement refléter immédiatement une mise à jour acceptée
État final du paiement
→ doit être confirmé par le backend faisant autoritéLes tests doivent refléter les exigences métier plutôt qu’une règle universelle selon laquelle « le cache est bon » ou « le cache est mauvais ».
Tester les mises à jour optimistes face au refus
Les interfaces optimistes améliorent la réactivité, mais elles créent des états supplémentaires.
Exemple :
L’utilisateur fait passer la quantité d’un article de 1 à 3
→ l’interface affiche immédiatement 3
→ le serveur indique que seuls 2 articles sont disponiblesLe comportement attendu doit être défini :
Revenir à 1
ou
afficher 2 comme maximum accepté
ou
demander à l’utilisateur de confirmer le panier ajustéTestez :
- l’état optimiste,
- la réponse acceptée,
- la réponse refusée,
- le rollback,
- l’annonce de l’erreur,
- l’interaction pendant l’état d’attente.
Ne laissez pas le comportement de rollback implicite.
Les tests navigateur doivent couvrir les parcours, pas les détails d’implémentation
Un test navigateur doit décrire l’objectif de l’utilisateur :
test('customer can complete checkout after correcting changed stock', async ({
page,
}) => {
await page.goto('/en/products/example-product');
await page.getByRole('button', {
name: /add to basket/i,
}).click();
await page.getByRole('link', {
name: /checkout/i,
}).click();
await page.getByLabel(/delivery address/i)
.selectOption('address-1');
await page.getByRole('button', {
name: /place order/i,
}).click();
await expect(
page.getByRole('alert'),
).toContainText(/quantity has changed/i);
await page.getByRole('button', {
name: /accept changes/i,
}).click();
await expect(page).toHaveURL(/\/orders\/[^/]+\/confirmation/);
await expect(
page.getByRole('heading', {
name: /order confirmed/i,
}),
).toBeVisible();
});Cet exemple emploie Playwright ; adaptez les routes, les sélecteurs et la syntaxe à l’application réelle.
Préférez les sélecteurs accessibles aux détails CSS d’implémentation.
Contrôler délibérément l’état du backend
Les tests navigateur deviennent peu fiables lorsqu’ils dépendent de données partagées non maîtrisées.
Utilisez une approche établie pour les données de test :
- préparation via une API de test,
- fixtures déterministes,
- base de données de test isolée,
- factories dédiées,
- stratégie approuvée de réinitialisation de la base.
Le test doit connaître :
- le stock du produit,
- les autorisations de l’utilisateur,
- l’état du panier,
- le mode de défaillance du backend,
- le résultat attendu de la commande.
Ne dépendez pas de comptes proches de la production partagés par plusieurs tests.
N’exécutez pas de préparation destructive contre un environnement réel.
Simuler les défaillances à des frontières contrôlées
Parmi les points utiles d’injection des défaillances :
Adaptateur API
Fournisseur d’authentification
Gateway de paiement
Chargeur de données
Service de téléversement de fichiers
Fournisseur de feature flags
Endpoint d’état de la fileÉvitez une interception trop large qui simulerait accidentellement des requêtes sans rapport.
Un test doit rendre la défaillance attendue évidente :
await checkoutApi.respondOnce({
status: 409,
body: {
code: 'basket_changed',
},
});Affirmez ensuite le comportement de récupération correspondant.
Ne pas mocker l’application entière
Un excès de mocks produit des tests qui confirment seulement que les mocks sont d’accord avec l’implémentation.
Un test de composant peut remplacer sa gateway.
Un test d’intégration de route doit employer davantage du routage réel et du comportement de sérialisation.
Un test de parcours navigateur doit exercer le vrai frontend et un environnement backend contrôlé.
Choisissez la plus petite frontière qui prouve encore le risque visé.
Utiliser des tests de contrat entre Next.js et le backend
Lorsque Next.js consomme une API Symfony, Laravel ou tout autre backend, les deux côtés doivent s’accorder sur :
- le statut HTTP,
- l’authentification,
- la structure de la requête,
- la structure de la réponse,
- les codes d’erreur,
- les valeurs nullables,
- les formats de date,
- la pagination,
- l’idempotence,
- le versionnement.
Les tests de contrat peuvent s’appuyer sur :
- un schéma d’API,
- des fixtures partagées,
- des exemples capturés et approuvés,
- des tests du fournisseur et du consommateur,
- des tests d’intégration directs.
Un test utile vérifie davantage qu’une réponse 200 :
it('maps a completed checkout response', async () => {
const fetcher = createFetchStub({
status: 201,
json: {
status: 'completed',
orderId: 'order-123',
confirmationNumber: 'ORD-123',
},
});
const gateway = new HttpCheckoutGateway(
'https://backend.test',
fetcher,
);
await expect(
gateway.submit({
basketId: 'basket-1',
deliveryAddressId: 'address-1',
idempotencyKey: 'checkout-attempt-1',
}),
).resolves.toEqual({
status: 'completed',
orderId: 'order-123',
confirmationNumber: 'ORD-123',
});
});Testez également les véritables structures d’erreur du fournisseur.
Les envois en double exigent l’idempotence
Désactiver un bouton réduit les doublons accidentels, mais n’offre pas une protection complète.
Les doublons peuvent encore provenir :
- des nouvelles tentatives du navigateur,
- des proxys réseau,
- de l’actualisation par l’utilisateur,
- de plusieurs onglets,
- d’une livraison répétée depuis une file,
- de réponses retardées.
Pour les mutations ayant des conséquences, le backend doit prendre en charge une clé d’idempotence ou une contrainte métier équivalente.
Le test frontend doit vérifier :
Une tentative de l’utilisateur
→ une clé d’idempotence stable
→ les nouvelles tentatives réutilisent la même clé
→ une nouvelle tentative intentionnelle reçoit une nouvelle cléLe backend reste responsable de l’application de l’idempotence.
Tester délibérément les conditions de concurrence
Une condition de concurrence fréquente survient lorsque l’utilisateur modifie sa sélection tandis qu’une requête précédente reste en attente.
Exemple :
L’utilisateur recherche « symfony »
→ la requête A démarre
→ l’utilisateur remplace la recherche par « nextjs »
→ la requête B démarre
→ la requête B se termine
→ la requête A se termine plus tardLa page ne doit pas remplacer le résultat récent par des données obsolètes.
Testez un ordre d’achèvement inversé :
it('does not display an older search response', async () => {
const first = createDeferred();
const second = createDeferred();
const search = vi
.fn()
.mockReturnValueOnce(first.promise)
.mockReturnValueOnce(second.promise);
render();
await userEvent.type(
screen.getByRole('searchbox'),
'symfony',
);
await userEvent.clear(
screen.getByRole('searchbox'),
);
await userEvent.type(
screen.getByRole('searchbox'),
'nextjs',
);
second.resolve(resultFor('nextjs'));
expect(
await screen.findByText('Next.js result'),
).toBeVisible();
first.resolve(resultFor('symfony'));
expect(
screen.queryByText('Symfony result'),
).not.toBeInTheDocument();
});Employez l’annulation, l’identité de requête ou la gestion d’état adaptée à l’implémentation.
L’hydratation et les différences d’environnement demandent de l’attention
Un code peut fonctionner pendant une navigation côté client et échouer lors d’une requête directe de la page.
Parmi les causes fréquentes :
- l’accès à
windowpendant l’exécution serveur, - l’utilisation différente de l’heure courante côté serveur et côté client,
- les différences de formatage selon la locale,
- des valeurs aléatoires pendant le rendu,
- l’accès au stockage du navigateur avant le montage,
- des feature flags divergents entre serveur et client.
Protégez les pages importantes avec des tests qui incluent :
Navigation directe
Navigation côté client
Actualisation
Retour et Suivant
Locale différente
État authentifié et anonymeNe masquez pas les avertissements d’hydratation sans identifier le désaccord.
Tester les données vides, partielles et inattendues
Les données de production sont rarement aussi complètes que le suggèrent les fixtures.
Testez :
- les listes vides,
- les images optionnelles absentes,
- les enregistrements liés supprimés,
- les champs de profil nullables,
- une locale non prise en charge,
- des autorisations partielles,
- des valeurs d’énumération inconnues,
- les anciens enregistrements du backend,
- les textes très longs,
- les valeurs zéro et maximales.
Une page ne doit pas planter parce qu’une relation optionnelle est absente.
Dans le même temps, les données métier obligatoires ne doivent pas devenir silencieusement optionnelles uniquement pour satisfaire un test.
L’accessibilité fait partie des tests de parcours
Un parcours est incomplet s’il ne fonctionne qu’avec une souris.
Les tests de composants et de navigateur doivent vérifier, lorsque c’est pertinent :
- les noms accessibles,
- les labels de formulaire,
- le déplacement du focus,
- l’activation au clavier,
- l’annonce des erreurs de validation,
- les résumés d’erreurs,
- le focus dans les modales,
- l’annonce du chargement.
Pour l’échec d’un envoi de formulaire, vérifiez que l’utilisateur peut localiser l’erreur et poursuivre.
Ne vous appuyez pas uniquement sur des snapshots visuels.
Les tests visuels sont utiles, mais limités
Les tests de régression visuelle peuvent protéger :
- les changements majeurs de mise en page,
- les sections responsive absentes,
- les squelettes de chargement cassés,
- les débordements,
- les thèmes incorrects.
Ils ne prouvent pas :
- l’autorisation,
- l’exactitude des données de l’API,
- l’idempotence,
- la réussite de l’envoi,
- les noms accessibles,
- le comportement de nouvelle tentative.
Utilisez les tests visuels comme un niveau parmi d’autres, pas comme un substitut aux assertions comportementales.
Tester le comportement responsive du parcours
Un parcours de commande qui fonctionne sur ordinateur peut échouer sur mobile parce que :
- le bouton d’envoi est masqué sous un élément fixe,
- le clavier recouvre les actions du formulaire,
- un résumé d’erreur se trouve hors du viewport,
- une boîte de dialogue ne peut pas défiler,
- la sélection d’adresse nécessite un survol,
- la navigation se ferme de manière inattendue.
Utilisez un petit nombre de viewports représentatifs pour les parcours critiques.
Ne dupliquez pas chaque test navigateur sur de nombreuses dimensions.
Choisissez les combinaisons selon le risque.
Garder les tests navigateur ciblés et indépendants
Un test navigateur ne doit pas dépendre du fait qu’un test précédent a créé des données.
Préférez :
Préparer un état contrôlé
→ exécuter un parcours
→ affirmer son résultat
→ nettoyer au moyen de l’infrastructure de test établieÉvitez :
Le test 1 crée un compte
→ le test 2 attend ce compte
→ le test 3 modifie le même compteLes parcours séquentiels partagés rendent les échecs difficiles à diagnostiquer.
Protéger la suite de tests contre une fausse confiance
Un test peut réussir pour une mauvaise raison.
Exemples :
- le motif d’interception de requête n’a jamais correspondu,
- l’assertion a vérifié un élément obsolète,
- le test a utilisé de manière inattendue une authentification mise en cache,
- l’envoi du formulaire n’a pas eu lieu,
- le test navigateur a été ignoré à cause de la configuration de l’environnement,
- l’erreur a été masquée par un gestionnaire d’exceptions trop large,
- le test a accepté n’importe quel statut inférieur à 500.
Utilisez des assertions qui prouvent que l’action importante a eu lieu.
Par exemple :
Vérifier le payload de la requête
Vérifier la route obtenue
Vérifier l’état enregistré dans le backend
Vérifier la confirmation visibleTous les tests n’ont pas besoin des quatre signaux, mais les parcours critiques doivent en inclure plusieurs.
Le comportement en production complète la stratégie de test
Les tests avant livraison ne peuvent pas reproduire toutes les combinaisons de :
- navigateur,
- réseau,
- volume de données,
- défaillance d’un fournisseur,
- calendrier du déploiement,
- état du cache,
- durée de session.
La vérification en production doit inclure :
- le taux d’erreur des routes,
- les appels API en échec,
- le taux d’achèvement des parcours,
- les points d’abandon du paiement,
- les boucles de redirection d’authentification,
- les exceptions côté client,
- les violations de contrat du backend,
- les percentiles de latence,
- l’efficacité du cache,
- la détection des actions dupliquées.
Une métrique utile n’est pas seulement :
L’API de paiement a renvoyé 201.C’est :
L’utilisateur a ouvert le passage de commande
→ envoyé la commande
→ atteint l’état de commande confirméeMesurez l’achèvement des parcours métier lorsque la politique de confidentialité et de supervision le permet.
Ajouter des identifiants de corrélation
Un parcours peut traverser :
Navigateur
→ serveur Next.js
→ API backend
→ prestataire de paiement
→ file de messagesSans identifiant de corrélation, une seule défaillance apparaît comme plusieurs logs sans rapport.
Utilisez un identifiant de requête ou de parcours établi, qui peut être propagé en toute sécurité.
N’exposez pas inutilement des identifiants internes sensibles.
Les tests peuvent vérifier que l’identifiant est :
- créé ou accepté à la bonne frontière,
- envoyé au backend,
- inclus dans des logs sûrs,
- renvoyé lorsque c’est pertinent,
- non régénéré de manière imprévisible au cours d’un même parcours.
Utiliser une matrice de tests fondée sur le risque
Toutes les pages ne méritent pas la même profondeur.
Classez les parcours :
Critiques
→ paiement, authentification, autorisations, changements irréversibles
Importants
→ publication de contenu, paramètres du compte, téléversement de documents
Secondaires
→ filtrage, préférences optionnelles, pages d’informationUn parcours critique peut nécessiter :
- des tests unitaires,
- des tests de défaillance de composants,
- des tests de contrat d’adaptateurs,
- des tests d’autorisation,
- un test navigateur de succès,
- un test navigateur de récupération,
- une supervision en production.
Une fonctionnalité secondaire peut n’exiger qu’une couverture ciblée des composants et de l’intégration.
L’objectif n’est pas une couverture identique.
L’objectif est une protection proportionnée.
Une séquence d’implémentation pragmatique
Phase 1 : cartographier le parcours
Documentez :
- le point de départ,
- l’objectif de l’utilisateur,
- les dépendances serveur,
- les interactions client,
- l’état final faisant autorité.
Phase 2 : créer la matrice des défaillances
Listez :
- les échecs de validation,
- les échecs d’autorisation,
- les défaillances de dépendances,
- les dépassements de délai,
- les actions dupliquées,
- les données obsolètes,
- les succès partiels.
Définissez la récupération attendue pour chacun.
Phase 3 : confirmer les frontières architecturales
Identifiez :
- les contrats applicatifs,
- les adaptateurs d’infrastructure,
- les modules réservés au serveur,
- les composants client,
- la validation des réponses à l’exécution.
Ajoutez des règles d’import applicables lorsque le projet les prend déjà en charge.
Phase 4 : ajouter les tests de bas niveau
Testez :
- les règles du domaine,
- les types de résultat des cas d’usage,
- le mapping des réponses,
- la classification des défaillances,
- le comportement des clés d’idempotence.
Phase 5 : ajouter les tests de comportement des composants
Testez :
- la validation,
- l’état en attente,
- la prévention des doublons,
- les erreurs récupérables,
- la conservation des saisies de l’utilisateur,
- le rollback optimiste.
Phase 6 : ajouter les tests d’intégration
Testez :
- l’authentification,
- l’autorisation,
- les réponses des routes,
- les contrats API,
- l’invalidation du cache,
- les redirections.
Phase 7 : ajouter les parcours navigateur critiques
Couvrez :
- un parcours réussi,
- un parcours de récupération important,
- un cas d’autorisation ou d’expiration de session lorsque le risque le justifie.
Phase 8 : ajouter les signaux de production
Mesurez :
- l’achèvement du parcours,
- le taux de défaillance technique,
- la réussite de la récupération,
- la latence,
- les violations de contrat,
- les actions dupliquées.
Ce que la revue de code doit examiner
Lors de la revue d’une modification Next.js, demandez :
- Des données réservées au serveur sont-elles importées par du code client ?
- Le composant appelle-t-il directement l’infrastructure ?
- Les données de l’API sont-elles validées à l’exécution ?
- Les contrôles d’autorisation sont-ils imposés côté serveur ?
- Que se passe-t-il lorsque la dépendance dépasse son délai ?
- L’utilisateur peut-il réessayer en toute sécurité ?
- La mutation peut-elle être envoyée deux fois ?
- La clé d’idempotence reste-t-elle stable entre les nouvelles tentatives ?
- Les données acceptées sont-elles invalidées ou actualisées ?
- Des requêtes obsolètes peuvent-elles écraser un état plus récent ?
- La navigation directe se comporte-t-elle comme la navigation côté client ?
- Les états de chargement, vide et d’erreur sont-ils accessibles ?
- Le test protège-t-il le comportement ou des détails d’implémentation ?
- Le parcours important complet est-il couvert quelque part ?
- La supervision de production révélera-t-elle une défaillance du parcours ?
Règles pour le développement assisté par l’IA
Il ne faut pas demander uniquement à un assistant de programmation IA :
Add tests for this page.Donnez-lui des responsabilités explicites :
Inspect the page, application contract, API adapter and existing tests.
Identify:
- success states,
- loading states,
- business rejections,
- technical failures,
- authentication changes,
- duplicate submission risks,
- cache invalidation,
- complete user workflow.
Use the existing test runner and browser framework.
Do not add packages.
Do not replace project-owned interfaces with direct fetch calls.
Do not assert private implementation details.
Add the smallest test layer that proves each risk.Avant d’accepter des tests générés par l’IA, vérifiez qu’ils peuvent échouer pour le défaut visé.
Un test qui ne peut pas échouer lorsque le comportement est cassé est de la documentation, pas une protection.
Conclusion d’ingénierie senior
Tester une application Next.js n’est pas une compétition visant à créer le plus grand nombre de tests.
Le but est de produire des preuves que l’architecture reste compréhensible, que les défaillances restent maîtrisées et que les utilisateurs peuvent accomplir un travail important.
Une stratégie mature protège généralement :
- le sens des dépendances,
- les responsabilités serveur et client,
- les contrats API à l’exécution,
- l’authentification et l’autorisation,
- le chargement et la récupération après erreur,
- les mutations idempotentes,
- le cache et la fraîcheur,
- les conditions de concurrence,
- les parcours navigateur critiques,
- l’achèvement des parcours en production.
La meilleure suite de tests ne cherche pas à exécuter toutes les combinaisons possibles dans le navigateur.
Elle place chaque risque au niveau fiable le plus bas et relie ces niveaux au moyen d’un petit nombre de parcours utilisateur réalistes.
C’est ce qui permet de refactoriser l’application en toute sécurité.
Cela rend également le développement assisté par l’IA plus sûr. Un outil d’IA peut générer rapidement des composants, des adaptateurs et des tests, mais le projet doit avoir défini au préalable les frontières, les attentes face aux défaillances et les parcours que le code généré doit préserver.
Une requête réussie n’est pas synonyme de parcours utilisateur réussi.
Une page rendue n’est pas synonyme d’architecture protégée.
Une suite de tests réussie n’a de valeur que si elle échoue face aux problèmes que l’entreprise doit réellement empêcher.
