Illustration for the postTester un route handler Next.js avec Jest

Tester un route handler Next.js avec Jest

Les “Route Handlers” sont la manière d’écrire des API côté serveur avec Next.js et le App Router. Dans l’ancienne architecture de Next.js, on les appelait des “API routes”.

Les Route Handlers utilisent Node.js, mais leur syntaxe très générique les rend aussi compatibles avec le “Edge runtime”, censé être plus léger et plus performant.

Voici un exemple de Route Handler qui renvoie le User-Agent du navigateur de l’utilisateur :

// app/api/browser/route.js
// l'URL sera "/api/browser"
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
export function GET() {
  return NextResponse.json({
    browser: headers().get('user-agent'),
  });
}

Ce point d’entrée est simple… mais pas tant que ça ! Il s’agit d’un bon exemple pour apprendre à tester un point d’entrée d’API Next.js avec le test runner Jest.

Malheureusement, au moment où nous écrivons cet article, les tests des Route Handlers ne sont pas documentés, et ils sont plus difficiles à mettre en oeuvre que l’on pourrait le penser.

Configurons donc ensemble Jest pour écrire un test unitaire validant ce point d’entrée d’API.

Vous êtes pressé ? Voici la solution !

Tester un route handler est un chemin semé d’embuches. Pour vous faire gagner du temps, voici un test qui fonctionne comme attendu :

/**
 * @jest-environment node
 */
import { expect, it } from "@jest/globals";
import { testApiHandler } from 'next-test-api-route-handler';
import { GET } from "./route"
it("returns the user agent", async () => {
	await testApiHandler({
 	   appHandler: { GET },
 	   requestPatcher(request) {
 	       request.headers.set('user-agent', 'Mozilla/5.0')
 	   },
 	   async test({ fetch }) {
 	       const data = await (await fetch()).json()
 	       expect(data).toEqual({ browser: "Mozilla/5.0" })
 	   }
	})
})

Le test fonctionne bien, mais deux points vont vous paraître étranges :

On répond à ces questions dans la suite de l’article.

Étape 1 : configurer Jest

La documentation officielle de Next.js nous explique comment configurer Jest.

Le point le plus difficile est la gestion de la compilation, surtout si l’on utilise TypeScript.

En résumé il faut exécuter la commande suivante dans votre projet Next.js pour installer toutes les dépendances :

npm install -D jest jest-environment-jsdom ts-node

Puis créer un fichier jest.config.mjs en utilisant le package next/jest qui va gérer toute la configuration pour nous :

// jest.config.mjs
// le ".mjs" permet d'utiliser des imports/exports ESM
import nextJest from 'next/jest.js'
const createJestConfig = nextJest({
  dir: './',
})
const config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',
}
export default createJestConfig(config)

Le package next/jest nous facilite grandement le travail, cependant vous remarquerez que cette configuration est optimisée pour tester des composants React, et non des points d’entrée d’API !

Il va falloir quelques étapes supplémentaire pour écrire des tests pour les Route Handlers.

Étape 2 : créer le test

Écrivons un premier test pour notre route handler, en suivant notre intuition. Attention, il ne va pas fonctionner tout de suite, loin de là !

Encore deux étapes seront nécessaires :

// app/api/route.test.js
import { expect, it } from "@jest/globals";
import { GET } from "./route"
it("returns the user agent", async () => {
	// ❌ Ce test est écrit intuitivement,
	// mais il ne fonctionnera pas comme attendu
	const req = new Request(
		'http://localhost:3000/api/browser', 
		{
			headers: new Headers({'user-agent': 'Mozilla/5.0'})
		})
	const res = await GET(req)
	expect(res.body).toEqual({
		browser: "Mozilla/5.0"
	})
})

Si vous lancez ce test, vous aurez une erreur laissant supposer que les objets Response et Request ne sont pas définis. En effet, vous êtes dans un environnement jsdom, qui ne définit pas ces variables globales, alors que nous souhaitons exécuter le test dans un environnement Node.js.

En théorie, on pourrait résoudre cette erreur avec un polyfill de l’API Fetch, mais cela n’est pas recommandé car vous vous heurterez à d’autres problèmes avec toutes les fonctions fournies par Node.js (fs par exemple pour lire des fichiers).

Étape 3 : utiliser le bon environnement

L’entrée testEnvironment: jsdom dans le fichier de configuration de Jest indique que les tests doivent simuler un DOM avec jsdom. Cela est très pratique pour les composants React, mais pas du tout pertinent pour des tests backend.

Voici deux façons de configurer Jest pour utiliser l’environnement Node.js. Attention, la première n’est pas compatible avec next/jest !

Première solution idéale, mais qui ne fonctionne pas : l’option “projects”

Jest fournit une option projects, qui nous permet de gérer plusieurs configurations, ici une pour React, et une pour Node.js.

Voici à quoi pourrait ressembler notre configuration.

projects: [
    {
      displayName: "React",
      testEnvironment: "jsdom",
      testMatch: [
		// Tous les tests sauf ceux de dossiers nommés "api"
		// ou finissant en "api.test.js"
        "**/!(*.api).test.[jt]s?(x)",
        "!**/api/**",
      ],
    },
    {
      displayName: "server",
      testEnvironment: "node",
      testMatch: [
		// Tous les tests du dossier "api"
		// ou finissant en "api.test.js"
        "**/*.api.test.[jt]s?(x)",
        "**/api/**/*.test.[jt]s?(x)",
      ],
    },

C’est la solution recommandée pour tester une application fullstack. Malheureusement, elle ne fonctionne pas bien avec le module next/jest, qui ne supporte pas l’option projects.

Solution palliative : la direction “jest-environment”

On peut changer l’environnement d’un test en utilisant une directive @jest-environment node en commentaire au début du fichier. Pas besoin de changer la configuration globale dans ce cas.

Ajoutez le commentaire suivant à votre test :

/**
 * @jest-environment node
 */
import { expect, it } from "@jest/globals";
// ...la suite du fichier

Votre test ne va toujours pas fonctionner, mais l’erreur va changer. Le test devrait désormais tiquer sur l’appel à headers().

C’est bon signe ! Nous allons finaliser le test dans l’étape suivante.

Étape finale : simuler le contexte de la requête HTTP

Dans un Route Handler, on peut accéder à la requête HTTP de deux façons :

Le problème est que notre test est écrit pour gérer le premier scénario, alors que notre composant utilise la seconde approche !

Première solution idéale, mais qui ne marche toujours pas : utiliser un mock

Dans un monde idéal, on pourrait utiliser un mock via jest.mock pour simuler ces fonctions dans notre test, comme cela :

jest.mock('next/headers', () => ({
  headers: jest.fn(),
}));

On peut alors contrôler le contenu de la fonction headers() pendant le test. Cependant le système de compilation de Next.js semble interférer avec les mocks.

Seconde solution qui fonctionne correctement : utiliser une librairie tierce

La librairie next-test-api-route-handler permet de contrôler entièrement le test d’un route handler. Il s’agit d’une solution plus lourde qu’un route handler, mais qui à l’avantage de fonctionner parfaitement !

Le contenu de notre test ressemblera finalement à ceci :

// fonction fournie par next-test-api-route-handler (NTARH)
await testApiHandler({
    appHandler: { GET },
	// On simule la requête avec le bon user agent
    requestPatcher(request) {
        request.headers.set('user-agent', 'Mozilla/5.0')
    },
	// On test le point d'entrée d'API en simulant un appel fetch
    async test({ fetch }) {
        const data = await (await fetch()).json()
        expect(data).toEqual({ browser: "Mozilla/5.0" })
    }
})

Conclusion : privilégiez des tests end-to-end pour les Route Handlers

Tester un route handler Next.js avec Jest s’avère finalement très difficile !

En pratique, il peut être plus efficace de privilégier des tests end-to-end pour les applications Next.js. Cela vaut pour les Route Handlers comme pour les pages écrites avec des React Server Components.

La dimension fullstack de ce frameowrk, avec une forte intégration entre client et serveur, fait que son fonctionnement est relativement complexe, et cela rend les tests unitaires plus difficiles à mettre en oeuvre.

On peut choisir de limiter les tests unitaires aux fonctions indépendantes de Next.js. Mais si vous souhaitez vraiment utiliser Jest pour tester des Route Handlers, cet article vous aura donné quelques pistes pour implémenter vos tests unitaires !

Références

Vous avez apprécié cette ressource ?

Découvrez toutes nos formations Next.js, Astro.js et LangChain en présentiel ou en distanciel

Voire toutes les ressources

Partager sur Bluesky Partager sur X
Flux RSS