Abschnitt 17 · Lektion 4
Einen Endpoint testen
Eine Route zu testen heißt, sie wirklich über HTTP anzufragen. Ohne Paket, mit dem, was Node mitbringt. Vorher muss aber eine Entscheidung fallen, die weiter unten liegt, als man denkt.
Erst die Trennung, dann der Test
Bis Lektion 16.6 hat deine server.js zwei Dinge getan: die Anwendung aufgebaut und den Server
gestartet. Für den Betrieb ist das in Ordnung. Für einen Test ist es das Ende.
import express from "express";
// So nicht: die Datei baut die Anwendung UND startet den Server.
const app = express();
app.get("/", (req, res) => res.send("ok"));
app.listen(3000); // Der Import allein startet schon einen Server auf Port 3000.
import "./gemischt.js";
console.log("Der Import ist durch, und ein Server hoert bereits auf 3000.");
console.log("Ein Test koennte sich hier keinen freien Port geben lassen,");
console.log("er kaeme an den Server gar nicht heran, und enden wuerde er nie.");
// Nur damit dieses Beispiel ueberhaupt aufhoert. Genau das ist das Problem.
setTimeout(() => process.exit(0), 100); Ein Test muss die Anwendung importieren, sonst kann er sie nicht anfragen. Beim Import läuft app.listen
mit, und ab da hast du einen Server auf einem festen Port, an den du nicht herankommst, den du nicht
schließen kannst und der den Testprozess am Leben hält, bis ihn jemand abbricht.
Die Lösung ist eine Zeile Umbau. app.js baut die Anwendung und gibt sie zurück. server.js
importiert sie und ruft listen auf. Der Betrieb startet server.js, der Test importiert app.js,
und beide bekommen genau das, was sie brauchen.
Nebenbei fällt dabei noch etwas ab: Wenn die Anwendung ihre Datenbank als Parameter bekommt
(baueApp(db)) statt sie selbst zu öffnen, kann der Test ihr eine eigene unterschieben. Das ist der
zweite Teil dieser Lektion.
Ein Test von Anfang bis Ende
import express from "express";
// So: die Datei baut die Anwendung und startet nichts.
export function baueApp() {
const app = express();
app.get("/", (req, res) => res.send("ok"));
return app;
} import test from "node:test";
import assert from "node:assert/strict";
import { baueApp } from "./getrennt.js";
test("antwortet auf der Startseite", async () => {
// Port 0 heisst: such dir einen freien.
const server = baueApp().listen(0);
await new Promise((fertig) => server.once("listening", fertig));
const adresse = `http://127.0.0.1:${server.address().port}`;
console.log(`Der Test hat sich einen freien Port geben lassen: ${server.address().port > 0}`);
const antwort = await fetch(adresse);
assert.equal(antwort.status, 200);
assert.equal(await antwort.text(), "ok");
await new Promise((fertig) => server.close(fertig));
}); Vier Schritte, und keiner davon braucht ein Paket.
listen(0) ist der Trick. Port 0 heißt nicht „Port null”, sondern „such dir einen freien”. Das
Betriebssystem sucht, und server.address().port sagt danach, welcher es geworden ist. Damit können
beliebig viele Testdateien gleichzeitig laufen, ohne sich in die Quere zu kommen, und node --test
tut das von sich aus.
Auf listening warten, denn listen ist asynchron. Wer sofort danach anfragt, bekommt manchmal
eine Antwort und manchmal einen abgelehnten Verbindungsversuch, und dieser Test ist dann launisch
statt kaputt, was schlimmer ist.
Mit fetch anfragen. Das ist dieselbe Funktion, die du im Browser kennst, und seit Node 18 ist
sie eingebaut. Für einen POST kommen method, headers und body dazu, genau wie überall sonst.
Am Ende schließen. Sonst hält der Server den Prozess offen, und der Testlauf endet nicht. Auch
server.close ist asynchron und meldet sich über einen Rückruf, wenn wirklich alles zu ist.
Jeder Test bringt seine eigene Datenbank mit
import { describe, it, beforeEach } from "node:test";
import assert from "node:assert/strict";
import { DatabaseSync } from "node:sqlite";
function baueDatenbank() {
const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE buecher (id INTEGER PRIMARY KEY, titel TEXT NOT NULL)");
db.prepare("INSERT INTO buecher (titel) VALUES (?)").run("Node in der Praxis");
return db;
}
describe("eine Datenbank fuer alle Tests", () => {
const db = baueDatenbank();
it("legt ein Buch an", () => {
db.prepare("INSERT INTO buecher (titel) VALUES (?)").run("Testen ohne Angst");
assert.equal(db.prepare("SELECT count(*) AS n FROM buecher").get().n, 2);
});
it("faengt mit genau einem Buch an", () => {
assert.equal(db.prepare("SELECT count(*) AS n FROM buecher").get().n, 1);
});
});
describe("eine frische je Test", () => {
let db;
beforeEach(() => {
db = baueDatenbank();
});
it("legt ein Buch an", () => {
db.prepare("INSERT INTO buecher (titel) VALUES (?)").run("Testen ohne Angst");
assert.equal(db.prepare("SELECT count(*) AS n FROM buecher").get().n, 2);
});
it("faengt mit genau einem Buch an", () => {
assert.equal(db.prepare("SELECT count(*) AS n FROM buecher").get().n, 1);
});
}); Dieselbe Falle wie in Lektion 17.2, nur teurer. Die erste Gruppe teilt sich eine Datenbank: Der erste Test legt ein Buch an, der zweite zählt drei statt zwei und wird rot. Was er meldet, hat mit seiner eigenen Behauptung nichts zu tun.
beforeEach läuft vor jedem Test der Gruppe, before einmal vor allen. Für alles, was ein Test
verändern kann, willst du beforeEach. Für teure Dinge, die niemand anfasst, reicht before.
Dasselbe gilt für afterEach und after beim Aufräumen, und dort gehört das Schließen von Server
und Datenbank hin.
:memory: ist für Tests das Richtige. Die Datenbank entsteht im Arbeitsspeicher, ist in
Millisekunden fertig, und mit dem Prozess ist sie wieder weg. Wenn du eine Datei brauchst, dann eine
im temporären Verzeichnis mit einem eindeutigen Namen. Was du nie anfasst, ist die echte
Datenbank der Anwendung: Ein Test, der Daten löscht, ist irgendwann ein Test, der die falschen Daten
löscht.
Die Reihenfolgefalle
Zum Schluss der Punkt, an dem die meisten Testbestände langsam verrotten.
Es ist verführerisch, Tests aufeinander aufbauen zu lassen: Der erste legt an, der zweite ändert, der dritte löscht. Das liest sich wie eine Geschichte und spart Vorbereitung.
Nur sind das keine Tests mehr, sondern ein Ablauf. Wird der zweite rot, laufen der dritte und alle
weiteren ins Leere und melden Fehler, die es gar nicht gibt. Wer einen davon einzeln laufen lässt,
sieht ihn scheitern, obwohl der Code stimmt. Und node --test darf sie nicht mehr in anderer
Reihenfolge oder parallel ausführen, was es sonst gern täte.
Ein Test muss allein laufen können. Wenn er das nicht kann, fehlt ihm ein beforeEach.
Zum Mitnehmen
Die Anwendung in einer Datei, das Zuhören in einer anderen. Erst dann kann ein Test sich einen freien Port geben lassen, seine eigene Datenbank mitbringen und danach alles wieder wegräumen.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.