La plupart des équipes qui veulent tester une application Next.js commettent la même erreur : elles cherchent l'outil unique qui couvrira tout. Il n'existe pas. Depuis l'arrivée de l'App Router et des Server Components, la question n'est plus « Jest ou autre chose », mais « qu'est-ce que je teste en mémoire, et qu'est-ce que je teste dans un vrai navigateur ».
La réponse tient en deux outils qui ne se marchent pas dessus : Vitest pour l'unitaire et l'intégration, Playwright pour le bout-en-bout. Vitest valide vos Server Actions, vos schémas Zod et vos composants synchrones en quelques millisecondes. Playwright rejoue les parcours réels — connexion, soumission de formulaire, redirection de paiement — dans un Chromium piloté.
Cet article pose la frontière exacte entre les deux, avec la config de départ, un exemple de chacun, et une règle simple pour décider où placer chaque test. Le tout appliqué sur la stack qu'on utilise en production : Next.js, TypeScript, Zod.
Pourquoi tester une application Next.js demande deux outils
Un test unitaire doit tourner en dizaines de millisecondes, sans navigateur, sans base de données. Un test end-to-end doit prouver qu'un utilisateur peut vraiment aller du clic à la confirmation. Ce sont deux objectifs contradictoires : la vitesse contre le réalisme. Vouloir les fusionner dans un seul outil donne des suites lentes que personne ne lance en local.
Vitest occupe la couche rapide. C'est le remplaçant natif de Vite : démarrage à froid quasi instantané, ESM sans transpilation manuelle, et une API presque identique à celle de Jest — vos describe, it, expect fonctionnent tels quels. Depuis 2024, tous les guides de tests publiés par l'équipe Next.js utilisent Vitest par défaut.
Playwright occupe la couche lente mais fidèle. Il pilote de vrais moteurs de rendu (Chromium, Firefox, WebKit), gère les cookies, le middleware et le router Next.js sans mock. C'est le seul moyen fiable de tester un flux d'authentification complet ou une redirection Stripe. La division du travail est nette, et c'est précisément ce qui rend la suite maintenable.
Installer Vitest dans un projet Next.js
Le plus rapide est de partir de l'exemple officiel avec create-next-app --example with-vitest. En manuel, on installe vitest, @vitejs/plugin-react, jsdom, @testing-library/react et @testing-library/dom en dépendances de développement, puis on crée un vitest.config.mts.
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
// jsdom simule le DOM en mémoire : pas de vrai navigateur,
// donc des tests qui tournent en millisecondes plutôt qu'en secondes.
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true, // évite d'importer describe/it/expect partout
},
})
Le point de vigilance : l'environnement jsdom est indispensable pour tout ce qui touche au rendu React, mais inutile pour tester une fonction pure ou un schéma Zod. Vitest permet de surcharger l'environnement fichier par fichier avec un commentaire // @vitest-environment node, ce qui accélère les tests qui n'ont pas besoin du DOM. Sur une suite de plusieurs centaines de tests, ce détail change réellement le temps de feedback.
Ce que Vitest teste bien, et ce qu'il ne peut pas
Vitest brille sur tout ce qui ne dépend pas d'un navigateur. Une Server Action est, techniquement, une fonction asynchrone : vous l'importez et vous l'appelez directement dans le test, en lui passant un FormData construit à la main. Les schémas Zod se testent en vérifiant que safeParse accepte les entrées valides et rejette les autres avec le bon message — c'est le complément naturel de la validation des données avec Zod que vous avez mise dans vos actions. Les composants synchrones, serveur ou client, se rendent avec Testing Library et s'assertent sur le DOM produit.
Là où ça coince : les Server Components asynchrones. React ne supporte pas encore leur rendu isolé côté test, et Vitest hérite de cette limite. Un composant serveur qui fait un await db.query(...) ne peut pas être monté proprement en unitaire aujourd'hui.
Playwright pour les parcours réels
Playwright teste ce qu'un utilisateur fait vraiment. On part de create-next-app --example with-playwright, puis on lance npm run build && npm run start avant npx playwright test — les tests E2E doivent viser un build de production, pas le serveur de dev, sinon vous mesurez des comportements qui n'existeront jamais en ligne.
import { test, expect } from '@playwright/test'
// On teste le parcours complet, pas une fonction isolée :
// le formulaire, la requête réseau réelle, la redirection.
test('un utilisateur peut se connecter', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('dev@kreio.fr')
await page.getByLabel('Mot de passe').fill('motdepasse-test')
await page.getByRole('button', { name: 'Se connecter' }).click()
await expect(page).toHaveURL('/dashboard')
})
Playwright est la seule couche capable de valider un flux d'authentification Next.js de bout en bout : dépôt du cookie de session, passage dans le middleware, protection des routes. Il gère aussi l'attente automatique — pas de sleep arbitraire —, la capture de screenshots au moindre échec, et le rejeu vidéo des tests qui cassent. Sur un tunnel de paiement ou un onboarding multi-étapes, c'est irremplaçable.
Où placer la frontière entre unitaire et E2E
La règle qu'on applique chez Kreio : Vitest teste tout ce qui n'a pas besoin d'un navigateur, Playwright teste tout ce qui en a besoin. La zone grise disparaît dès qu'on se pose la question « ce comportement dépend-il des cookies, du router ou du middleware ? ». Si oui, c'est du E2E.
| Ce que vous testez | Vitest | Playwright |
|---|---|---|
| Server Action comme fonction | oui | — |
| Schéma Zod / validation | oui | — |
| Composant synchrone (Testing Library) | oui | — |
| Composant serveur asynchrone | — | oui |
| Flux d'authentification | — | oui |
| Redirection de paiement | — | oui |
| Comportement dépendant du middleware | — | oui |
Le piège à éviter est l'inversion : tester la logique métier en E2E « parce que c'est plus proche du réel ». Un test Playwright qui vérifie un calcul de TVA est cent fois plus lent et cent fois plus fragile qu'un test Vitest équivalent. Gardez l'E2E pour les parcours ; gardez l'unitaire pour la logique. Cette discipline maintient la suite sous la minute en local.
Intégrer les tests dans la CI
Une suite de tests qui ne tourne pas automatiquement à chaque push ne sert à rien. Vitest s'exécute en premier dans le pipeline, car il échoue en quelques secondes s'il y a une régression logique. Playwright vient ensuite, après le build, avec ses navigateurs installés via npx playwright install --with-deps.
Le cache est le second levier : mettez en cache node_modules et les binaires Playwright entre les runs pour éviter de réinstaller Chromium à chaque exécution. Un pipeline bien découpé garde les tests sous les cinq minutes même sur une suite conséquente. Pour le détail du workflow complet, on a documenté notre pipeline CI/CD Next.js avec GitHub Actions.
En pratique
Pour tester une application Next.js sans tomber dans l'effet tunnel, commencez petit : une poignée de tests Vitest sur vos Server Actions et vos schémas Zod, puis un seul parcours Playwright — la connexion, presque toujours. Ces deux points couvrent déjà l'essentiel du risque de régression, et ils s'installent en une après-midi.
La progression naturelle consiste ensuite à ajouter un test unitaire à chaque bug corrigé, et un test E2E à chaque parcours critique livré. En quelques semaines, la suite devient un filet qui vous laisse refactorer sans peur. C'est là que le couple Vitest–Playwright prend tout son sens : l'un vous dit que votre logique est juste, l'autre que votre utilisateur peut vraiment l'utiliser.
Chez Kreio, agence Next.js basée à Évreux (Normandie), on applique ces patterns sur des projets clients en production. Besoin d'un audit de votre stack de tests ou d'un renfort tech ? Parlons-en.