Kodokon kodokon.com

Deine API testen: node:test, fetch und eine Testdatenbank

Teste deine Routen von Ende zu Ende mit node:test und fetch auf einem flüchtigen Port, wobei jeder Test seine eigene SQLite-Datenbank im Arbeitsspeicher bekommt.

9 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Node 20 bringt alles Nötige mit: node:test als Runner, node:assert/strict für die Zusicherungen, das globale fetch als HTTP-Client. Null Test-Abhängigkeiten. Und hier zahlt sich die Architektur aus der ersten Lektion aus: createApp({ db }) nimmt eine injizierte Datenbank entgegen, sodass jeder Test in wenigen Millisekunden eine vollständige, isolierte API starten kann.

BASH
node --test
node --test --watch
Der Runner findet die *.test.js-Dateien des Projekts von selbst.

Um die Routen von Ende zu Ende zu testen, musst du Express nicht mocken: Starte einen echten Server auf Port 0 - das System vergibt einen freien Port - und sprich ihn dann mit fetch an. Du übst genau das, was ein echter Client sehen wird: Routing, Middleware, Serialisierung, Statuscodes. Das ist der Geist von supertest, ohne die Abhängigkeit.

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: ein echter Server auf einem vom System vergebenen Port.

Die Testdatenbank: SQLite im Arbeitsspeicher (:memory:). Jeder Test baut sich über createDb seine eigene frische, leere Datenbank, die das Schema anwendet, und verwirft sie beim Verlassen wieder - keine zwischen Tests geteilten Daten, kein Aufräumskript. t.after garantiert, dass der Server auch dann geschlossen wird, wenn eine Zusicherung fehlschlägt.

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');
});
Ein End-to-End-Test: echter HTTP-Stack, Wegwerf-Datenbank.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Warum den Testserver mit app.listen(0) starten?
    • Port 0 deaktiviert das Netzwerk und beschleunigt die Tests
    • Das System vergibt einen freien Port: Parallele Tests streiten sich nie um denselben Port
    • Es ist der einzige Port, der außerhalb der Produktion erlaubt ist
    • Port 0 zwingt Express in den Testmodus
  2. Was ist der entscheidende Vorteil einer :memory:-Datenbank, die in jedem Test erstellt wird?
    • Sie behält die Daten zwischen Durchläufen, um sie zu vergleichen
    • Jeder Test startet aus einem leeren, isolierten Zustand, ohne Aufräumskript und ohne Störungen zwischen den Tests
    • Sie deaktiviert die SQL-Constraints, um die Einfügungen zu vereinfachen
  3. Warum den Server in t.after schließen statt am Ende des Testrumpfs?
    • t.after läuft auch dann, wenn eine Zusicherung fehlschlägt, und vermeidet verwaiste Server, die den Prozess am Beenden hindern
    • t.after läuft vor dem Test und bereitet den Server vor
    • Das ist reine Stilfrage, das Verhalten ist identisch