Kodokon kodokon.com

Tester son API : node:test, fetch et base de test

Testez vos routes de bout en bout avec node:test et fetch sur un port éphémère, chaque test recevant sa base SQLite en mémoire.

9 min · 3 questions

Ouvrir cette leçon dans Kodokon

Node 20 embarque tout le nécessaire : node:test comme lanceur, node:assert/strict pour les assertions, fetch global comme client HTTP. Zéro dépendance de test. Et c'est ici que l'architecture de la première leçon rapporte : createApp({ db }) accepte une base injectée, donc chaque test peut monter une API complète et isolée en quelques millisecondes.

BASH
node --test
node --test --watch
Le lanceur découvre seul les fichiers *.test.js du projet.

Pour tester les routes de bout en bout, inutile de simuler Express : montez un vrai serveur sur le port 0 - le système attribue un port libre - puis attaquez-le avec fetch. Vous exercez exactement ce que verra un client réel : routage, middlewares, sérialisation, codes de statut. C'est l'esprit de supertest, sans dépendance.

JAVASCRIPT
import { once } from 'node:events';

export async function startTestServer(app) {
  const server = app.listen(0);
  await once(server, 'listening');
  const { port } = server.address();
  const url = `http://127.0.0.1:${port}`;
  const close = () =>
    new Promise((resolve) => {
      server.close(resolve);
    });
  return { url, close };
}
helpers.js : un serveur réel sur un port attribué par l'OS.

La base de test : SQLite en mémoire (:memory:). Chaque test construit sa propre base vierge via createDb, qui applique le schéma, puis l'abandonne en sortant - aucune donnée partagée entre tests, aucun script de nettoyage. t.after garantit la fermeture du serveur même quand une assertion échoue.

JAVASCRIPT
import test from 'node:test';
import assert from 'node:assert/strict';
import { createDb } from '../src/db/connection.js';
import { createApp } from '../src/app.js';
import { startTestServer } from './helpers.js';

test('POST /api/users creates a user', async (t) => {
  const db = createDb(':memory:');
  const app = createApp({ db });
  const server = await startTestServer(app);
  t.after(() => server.close());

  const res = await fetch(`${server.url}/api/users`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      email: 'ada@example.com',
      name: 'Ada',
    }),
  });

  assert.equal(res.status, 201);
  const body = await res.json();
  assert.equal(body.email, 'ada@example.com');
});
Un test de bout en bout : vraie pile HTTP, base jetable.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Pourquoi démarrer le serveur de test avec app.listen(0) ?
    • Le port 0 désactive le réseau et accélère les tests
    • Le système attribue un port libre : les tests parallèles ne se disputent jamais le même port
    • C'est le seul port autorisé hors production
    • Le port 0 force Express en mode test
  2. Quel est l'avantage décisif d'une base :memory: créée dans chaque test ?
    • Elle persiste les données entre les exécutions pour comparaison
    • Chaque test part d'un état vierge et isolé, sans script de nettoyage ni interférence entre tests
    • Elle désactive les contraintes SQL pour simplifier les insertions
  3. Pourquoi fermer le serveur dans t.after plutôt qu'à la fin du corps du test ?
    • t.after s'exécute même si une assertion échoue, évitant les serveurs orphelins qui empêchent le processus de se terminer
    • t.after s'exécute avant le test, ce qui prépare le serveur
    • C'est purement stylistique, le comportement est identique