API Testing

Supertest

Supertest steuert Ihre Node HTTP-App im selben Prozess und verwandelt API-Tests in einfache Assertions für Status, Header und Body — kein Server zum Starten, kein Port zu verwalten, kein Netzwerk zu mocken.

beginner14 min readUpdated 16. Sept. 2026
posts.test.ts
ts
// posts.test.ts
import request from "supertest";
import { expect, test } from "vitest";
import app from "../src/app.js";

test("GET /posts returns a list", async () => {
  const res = await request(app)
    .get("/posts")
    .expect("Content-Type", /json/)
    .expect(200);

  expect(res.body).toEqual(
    expect.arrayContaining([expect.objectContaining({ id: 1 })]),
  );
});
Basiert auf
superagent
Läuft gegen
Ihr App-Objekt
Test-Runner
Vitest, Jest, node:test
Netzwerk
In-process, ephemeral Port
Stil
Chainable Assertions
Erstveröffentlichung
2011

Warum es wichtig ist

Warum Supertest unverzichtbar ist

Ihre App, kein Server

Richten Sie Supertest auf die exportierte App-Funktion, und es startet einen temporären Server für den Test und schließt diesen danach wieder. Es gibt keinen Port zu wählen und keinen Prozess zu überwachen.

Kettenartige Requests

Die Chain im superagent-Stil — Methode, set, send, query — liest sich wie der HTTP-Request, den Sie beschreiben, sodass Tests gleichzeitig als Dokumentation dienen.

Assertions in der Chain

expect(status), expect(header, value) und expect(body) schlagen fehl und hängen die echte Response an, was den Weg vom roten Test zur Fehlerursache verkürzt.

Das Gesamtbild

Drei Kernkonzepte für den Durchblick

Sie übergeben Supertest eine App, es erstellt die Request für Sie, und die Response können Sie wie jedes andere Objekt validieren.

Das App-Objekt

Import

Exportieren Sie den Request-Listener aus app.ts und behalten Sie listen() in server.ts. Tests importieren die App und öffnen niemals einen festen Port.

Der Request

Compose

get, post, set, send und query bauen einen HTTP-Aufruf. Objekte werden in JSON serialisiert und der passende Content-Type wird automatisch gesetzt.

Die Response

Assert

status, headers, body und text sind einfache Werte, sodass Sie diese mit demselben expect validieren, das Sie überall sonst verwenden.

HTML5 auf einen Blick

Die Supertest-Toolbox

request(app)

Übergeben Sie die App an Supertest und erhalten Sie einen kettebaren Request-Builder zurück.

Methoden

.get, .post, .put, .patch und .delete bilden die HTTP-Verben direkt ab.

Bodies

.send({...}) serialisiert in JSON und setzt den Content-Type für Sie.

Auth

.set('Authorization', ...) oder .auth() fügen Anmeldedaten zum Request hinzu.

Response

res.status, res.headers und res.body stehen für Assertions bereit.

Uploads

.attach() und .field() erstellen Multipart-Formular-Übermittlungen.

Ablauf

Der Ablauf eines Supertest-Tests

Jeder Supertest-Test folgt demselben Pfad: vom Import der App bis zum Bereinigen der berührten Daten.

  1. 1

    App importieren

    Importieren Sie den exportierten Request-Listener, nicht einen laufenden Server. Es ist dasselbe Objekt, das Ihr Production-Entry-Point verwendet.

  2. 2

    Request aufbauen

    Rufen Sie request(app) auf und chainen Sie die Methode, den Pfad, Header, Query und den Body, den Sie testen möchten.

  3. 3

    Senden und awaiten

    Awaiten Sie die Chain. Supertest startet einen temporären Server, sendet den Request und löst ihn mit der Response auf.

  4. 4

    Status und Header prüfen

    Prüfen Sie zuerst den Statuscode und den Content-Type; diese fangen Routing- und Serialisierungsfehler ab, bevor die Body-Assertions greifen.

  5. 5

    Body validieren

    Vergleichen Sie den geparsten Body mit der Struktur, die der Contract verspricht, und nutzen Sie das expect Ihres Test-Runners für komplexe Prüfungen.

  6. 6

    Bereinigen

    Setzen Sie die berührten Daten zurück und schließen Sie die Datenbank oder den Connection-Pool, damit der nächste Test von einem bekannten Zustand aus startet.

Der vollständige Leitfaden

Supertest: Alles was Sie wissen müssen

Was ist Supertest?

Supertest ist eine HTTP-Assertion-Library für Node.js. Sie nimmt ein App-Objekt – eine Express-App, eine Fastify-Instanz oder einen einfachen http Request-Listener – und ermöglicht es Ihnen, über eine chainable API Anfragen an dieses Objekt zu senden und anschließend die Antwort zu validieren.

Es basiert auf superagent, sodass Ihnen die Seite der Anfragen vertraut vorkommen wird, falls Sie diese Library bereits genutzt haben. Was Supertest ergänzt, ist die Ergonomie beim Testen: Eine App kann direkt anstelle einer URL übergeben werden, der Server-Lifecycle wird automatisch verwaltet und .expect() Assertions können an die Chain angehängt werden.

Wichtig zu verstehen ist, dass Supertest keinen Browser ausführt und nicht Ihren Produktionsserver startet. Es erstellt einen kurzlebigen HTTP-Server im selben Prozess, der an einen temporären Port auf localhost gebunden wird, sendet die Anfrage ab und fährt den Server wieder herunter, sobald die Antwort aufgelöst wurde. Ihr Test sieht den echten HTTP-Stack – Statuscodes, Header, Bodys, Serialisierung – ohne den Overhead und die Instabilität eines separaten Prozesses.

Warum In-Process-Tests?

Der Großteil der Schwierigkeiten beim HTTP-Testing resultiert aus dem Server, nicht aus dem Request. Ein separater Prozess benötigt einen Port, eine Wartezeit für den Start, einen Health Check und einen Teardown. Fixe Ports kollidieren in der CI, und ein Race Condition zwischen listen und dem ersten Request führt zu Fehlern, die wie Application-Bugs aussehen.

In-Process-Tests eliminieren all das. Es gibt keinen Prozess, der gestartet werden muss, also ist kein Readiness Check erforderlich. Es gibt keinen fixen Port, sodass parallele Testdateien sich nicht gegenseitig blockieren. Es gibt keine Netzwerkbarriere, wodurch ein Fehler direkt auf deinen Handler und nicht auf die Umgebung hinweist.

Du erhältst dennoch die vollständige Request-Pipeline: middleware, Routing, Body Parsing, Authentifizierung und Error Handling laufen exakt so ab wie in der Produktion, da es sich um denselben Code handelt. Was du aufgibst, sind die Dinge, die nur ein echter Browser oder ein echtes Netzwerk bieten können – die JavaScript-Ausführung, eine Rendering-Engine und das exakte Verhalten eines Proxys vor deiner App.

Exportiere die App, nicht den Listener

Damit Supertest deine App importieren kann, muss diese exportierbar sein. Ein häufiger Fehler besteht darin, die Routen zu definieren und listen im selben Modul aufzurufen. Dadurch wird der Server bereits in dem Moment gestartet, in dem die Datei importiert wird, was in deinen Tests zu belegten Ports führt.

Trenne diese beiden Zuständigkeiten:

// src/app.ts
import express from "express";
import { postsRouter } from "./routes/posts.js";

export const app = express();

app.use(express.json());
app.use("/posts", postsRouter);

app.use((err, req, res, next) => {
  res.status(err.status ?? 500).json({ error: err.code ?? "internal_error" });
});
// src/server.ts
import { app } from "./app.js";

app.listen(3000, () => console.log("listening on http://localhost:3000"));

Nun exportiert src/app.ts den Request-Listener und sonst nichts. Die Tests importieren ihn, und die Produktionsumgebung importiert ihn aus server.ts. Diese eine Trennung macht den Unterschied zwischen einer API, die du in Millisekunden testen kannst, und einer, die du jedes Mal komplett booten musst.

Das app von Express ist selbst eine Funktion mit der (req, res)-Signatur, was genau das ist, was Node’s http.createServer erwartet. Aus diesem Grund funktioniert request(app) ohne zusätzlichen Adapter. Fastify benötigt app.server oder ein await-basiertes app.ready(), während ein nackter Node-Handler direkt funktioniert.

Eine Anfrage stellen

Eine Anfrage beginnt mit request(app) und der HTTP-Methode. Alle von superagent unterstützten Methoden stehen zur Verfügung, und die Kette gibt dasselbe Request-Objekt zurück, sodass Aufrufe gestapelt werden können.

import request from "supertest";
import app from "../src/app.js";

await request(app).get("/posts");
await request(app).post("/posts").send({ title: "Hello" });
await request(app).patch("/posts/1").send({ title: "Updated" });
await request(app).delete("/posts/1");

send serialisiert ein Objekt zu JSON und setzt automatisch den Content-Type-Header. Ein String wird unverändert gesendet, was nützlich ist, wenn man absichtlich einen fehlerhaften Body testen möchte.

Header werden mit set gesetzt, entweder einzeln oder als Objekt. Query-Parameter lassen sich sauberer über query lösen, welche diese für Sie kodiert und anhängt.

await request(app)
  .get("/posts")
  .query({ page: 2, perPage: 10 })
  .set("Accept", "application/json")
  .set({ "X-Request-Id": "test-1" });

Die resultierende URL ist /posts?page=2&perPage=10. Wenn Sie einen Raw-Body benötigen – XML, ein einfacher String oder ein bewusst fehlerhaftes Payload – übergeben Sie set("Content-Type", ...) und send den String.

Assertions auf die Response

Die Response ist ein normales Objekt mit status, headers, body und text. Du kannst Assertions mit dem expect deines Test-Runners durchführen oder direkt in der Chain die .expect() von Supertest verwenden.

const res = await request(app).get("/posts").expect(200);

expect(res.headers["content-type"]).toMatch(/application\/json/);
expect(res.body).toHaveLength(3);

Das .expect() von Supertest ist praktisch, da es im Falle eines Fehlers die vollständige Response in der Fehlermeldung ausgibt. Das reicht meistens aus, um zu sehen, was schiefgelaufen ist, ohne dass man zusätzliche Log-Zeilen einfügen muss.

await request(app)
  .get("/posts")
  .expect("Content-Type", /json/)
  .expect(200);

.expect() akzeptiert einen Statuscode, einen Header-Namen samt Wert, einen Body für einen Deep-Equality-Vergleich oder eine Funktion, die die Response erhält und einen Fehler werfen kann. Die Funktionsform ist das „Escape Hatch“, wenn eine Assertion Logik benötigt.

await request(app)
  .get("/posts")
  .expect((res) => {
    if (!res.body.every((p: { id: number }) => p.id > 0)) {
      throw new Error("every post must have a positive id");
    }
  });

Bevorzuge für alles, was über die Statuszeile und den Content-Type hinausgeht, den expect des Runners. Dieser bietet bessere Diffs, unterstützt Matcher wie toMatchObject und arrayContaining und hält den Assertions-Stil konsistent mit dem Rest deiner Test-Suite.

Supertest mit deinem Test-Runner kombinieren

Supertest ist kein Test-Runner. Es erstellt Requests und kann Assertions auf Responses ausführen, aber es entdeckt keine Tests, bietet kein describe und it, mockt keine Module und erstellt keine Ergebnisberichte. Diese Aufgabe übernehmen Vitest, Jest oder node:test.

Die beiden lassen sich sauber kombinieren, da ein Supertest-Request „thenable“ ist. Durch das Awaiten wird die Response aufgelöst, sodass ein Test einfach eine async-Funktion ist.

import request from "supertest";
import { beforeEach, describe, expect, it } from "vitest";
import app from "../src/app.js";

describe("POST /posts", () => {
  beforeEach(async () => {
    await resetDatabase();
  });

  it("creates a post", async () => {
    const res = await request(app)
      .post("/posts")
      .send({ title: "Hello" })
      .expect(201);

    expect(res.body.title).toBe("Hello");
  });
});

Wenn du das Awaiten vergisst, wird der Test als bestanden gewertet, noch bevor der Request überhaupt gesendet wurde, und der Fehler taucht später als „unhandled rejection“ auf. Nutze await für jeden Request, selbst für diejenigen, deren einzige Assertion .expect() ist.

Authentifizierung testen

Die Authentifizierung besteht lediglich aus einem Header oder einem Cookie, weshalb beides einfach zu testen ist.

Verwenden Sie für Bearer-Token den Authorization Header. Signieren Sie ein Token mit demselben Test-Secret, das auch die App verwendet, anstatt den echten Identity Provider aufzurufen.

const token = await signTestToken({ sub: "user_1", scope: "read:posts" });

await request(app).get("/posts").expect(401);

await request(app)
  .get("/posts")
  .set("Authorization", `Bearer ${token}`)
  .expect(200);

Bei Session-Cookies behält request.agent(app) über mehrere Anfragen hinweg einen Cookie-Jar bei, was das Verhalten eines Browsers nach einem Login imitiert.

const agent = request.agent(app);

await agent
  .post("/login")
  .send({ email: "[email protected]", password: "secret" })
  .expect(204);

await agent.get("/me").expect(200);

Wenn Sie bereits einen Cookie-Wert haben, setzen Sie diesen direkt. Cookies werden als Array oder als durch Semikolons getrennter String übergeben.

await request(app).get("/me").set("Cookie", ["session=abc123"]).expect(200);

Testen Sie die Negativfälle genauso sorgfältig wie den Happy Path: kein Token, ein abgelaufenes Token, ein Token für eine andere Audience und ein gültiges Token ohne den erforderlichen Scope. Diese vier Tests bieten Ihnen einen weitaus besseren Schutz als ein einziger Erfolgsfall.

Daten aufbauen und bereinigen

Supertest hat keine feste Meinung zur Datenbank. Das bedeutet, dass eine gemeinsam genutzte Datenbank Zustände zwischen Tests „leaken“ wird, sofern sie nicht zurückgesetzt wird. Es gibt drei gängige Strategien.

Vor jedem Test zurücksetzen. Leeren Sie die Tabellen, die die Suite berührt, in beforeEach. Dies ist einfach und vorhersehbar, und der Aufwand ist für kleine Suites akzeptabel.

beforeEach(async () => {
  await db.query("TRUNCATE posts RESTART IDENTITY CASCADE");
  await db.query("INSERT INTO posts (id, title) VALUES (1, 'Seeded')");
});

Eine Transaktion pro Test. Wenn Ihre App und Ihr Test dieselbe Verbindung teilen, kapseln Sie jeden Test in einer Transaktion und führen in afterEach einen Rollback durch. Dies ist schnell und lässt die Datenbank unberührt, funktioniert jedoch nur, wenn die App denselben Client verwendet – was über HTTP nicht immer der Fall ist.

Eine frische Datenbank pro Testdatei. Starten Sie für die Datei eine In-Memory- oder containerisierte Datenbank, führen Sie die Migrationen aus und verwerfen Sie diese am Ende. Dies bietet die stärkste Isolation und ist der Ansatz, bei dem sich die meisten Integration-Suites entscheiden, allerdings auf Kosten eines langsameren ersten Tests.

Egal wofür Sie sich entscheiden: Schließen Sie den Connection Pool in afterAll. Ein offener Pool hält den Node-Prozess am Laufen und verwandelt eine erfolgreiche Suite in einen hängenden CI-Job.

Die “Unhappy Paths” testen

Eine Route ist erst dann vollständig getestet, wenn auch ihre Fehlerfälle geprüft wurden. Der Statuscode ist Teil des API-Vertrags, daher sollte er explizit überprüft werden.

await request(app).get("/posts/999").expect(404);
await request(app).post("/posts").send({}).expect(422);
await request(app).get("/admin").expect(403);

Ein Validierungsfehler sollte genau angeben, welches Feld fehlerhaft ist, und nicht nur, dass allgemein etwas schiefgelaufen ist.

const res = await request(app)
  .post("/posts")
  .send({ title: "" })
  .expect(422);

expect(res.body).toEqual({
  error: "validation_error",
  fields: { title: "required" },
});

Die Unterschiede sind entscheidend. 400 steht für eine fehlerhafte Anfrage, 401 bedeutet nicht authentifiziert, 403 bedeutet authentifiziert, aber nicht berechtigt, 404 bedeutet, dass die Ressource nicht existiert, und 422 bedeutet, dass der Body zwar geparst wurde, die Validierung jedoch fehlgeschlagen ist. Die Überprüfung des falschen Statuscodes ist ein Bug im Test, der einen Bug in der App verschleiert.

Datei-Uploads und multipart

Supertest erstellt multipart-Requests mit attach für Dateien und field für die dazugehörigen Formularfelder. Übergeben Sie entweder einen Buffer oder einen Pfad; wenn Sie einen Buffer verwenden, geben Sie einen Dateinamen an, damit der Server einen sinnvollen Namen erhält.

await request(app)
  .post("/users/1/avatar")
  .field("caption", "Profile picture")
  .attach("avatar", Buffer.from("fake-image"), "avatar.png")
  .expect(201);

Testen Sie auch die Ablehnungen: eine fehlende Datei, eine Datei, die das Größenlimit überschreitet, und ein nicht zulässiger MIME-Typ. Uploads sind eine der häufigsten Stellen, an denen sich Validierungslücken verstecken.

Pagination, Filterung und Sortierung testen

Query-Strings sind Teil des API-Vertrags und verdienen daher eigene Tests. query macht diese lesbar, und die Überprüfung der Länge sowie der Reihenfolge des Bodys hilft dabei, Off-by-one-Fehler und Bugs bei Standardwerten zu finden.

test("GET /posts paginates", async () => {
  await seedPosts(25);

  const page1 = await request(app)
    .get("/posts")
    .query({ page: 1, perPage: 10 })
    .expect(200);

  expect(page1.body).toHaveLength(10);
  expect(page1.body[0].id).toBe(1);

  const page3 = await request(app)
    .get("/posts")
    .query({ page: 3, perPage: 10 })
    .expect(200);

  expect(page3.body).toHaveLength(5);
});

Testen Sie die Grenzfälle, nicht nur den Normalfall: die erste Seite, die letzte Seite, eine Seite hinter dem Ende sowie einen ungültigen perPage, der entweder begrenzt oder abgelehnt werden sollte.

await request(app)
  .get("/posts")
  .query({ page: 999 })
  .expect(200)
  .expect((res) => {
    if (res.body.length !== 0) throw new Error("expected an empty page");
  });

await request(app).get("/posts").query({ perPage: 10_000 }).expect(400);

Filterung und Sortierung sind ebenso testbar. Genau hier schlagen sich fehlende Indizes oder ein falscher ORDER BY oft als subtile Bugs nieder.

const res = await request(app)
  .get("/posts")
  .query({ status: "published", sort: "-createdAt" })
  .expect(200);

expect(
  res.body.every((p: { status: string }) => p.status === "published"),
).toBe(true);

Redirects, Cookies und weitere HTTP-Details

Nicht jede Antwort besteht aus einem JSON-Body. Statuscodes wie 301, 302 und 304 sowie Header wie Location, Set-Cookie und Cache-Control stellen oft das gesamte zu testende Verhalten dar.

Standardmäßig folgt supertest keinen Redirects, was genau richtig ist, wenn man den Redirect selbst validieren möchte.

const res = await request(app).get("/old-posts").expect(301);

expect(res.headers.location).toBe("/posts");

Wenn die Redirect-Kette entscheidend ist, folgt redirects(1) einem Hop und löst mit der finalen Antwort auf.

await request(app).get("/old-posts").redirects(1).expect(200);

Cookies sind in set-cookie sichtbar, und das Jar des Agents ermöglicht es, zu prüfen, ob ein Login die richtigen Attribute gesetzt hat, ohne den Wert dekodieren zu müssen.

const res = await request.agent(app)
  .post("/login")
  .send({ email: "[email protected]", password: "secret" })
  .expect(204);

const cookie = res.headers["set-cookie"][0];
expect(cookie).toContain("HttpOnly");
expect(cookie).toContain("SameSite=Lax");

Bedingte Anfragen (Conditional Requests), Komprimierung und Caching-Header sind alle einen Test wert, wenn man sich auf sie verlässt, da ein Proxy oder CDN das Verhalten gerne ändert, wenn die Header falsch gesetzt sind.

Die Test-Suite beschleunigen

Eine Supertest-Suite ist normalerweise schnell, aber einige gute Gewohnheiten sorgen dafür, dass sie auch bei zunehmender Größe performant bleibt.

Wiederverwenden Sie aufwendige Setups in beforeAll und setzen Sie pro Test nur die veränderlichen Teile zurück. Wenn Sie einen Container starten oder ein Schema migrieren, anstatt dies für jeden einzelnen Test zu tun, können Sie bei einer großen Suite Minuten an Zeit einsparen.

Führen Sie Testdateien parallel aus. Vitest und Jest machen dies standardmäßig. Da jede Supertest-Anfrage einen ephemeren Port verwendet, gibt es keine Kollisionen, die gelöst werden müssten. Das Einzige, was Sie gewährleisten müssen, ist, dass die Dateien keine gemeinsamen Datenbankzeilen verwenden.

Verzichten Sie in Tests, die nur Daten lesen, auf nicht benötigte Arbeitsschritte. Wenn eine Route nur einen Benutzer und einen Post benötigt, seeden Sie nicht den gesamten Fixture-Satz. Kleinere Fixtures sind schneller zu erstellen und leichter nachvollziehbar.

# run one file while iterating
pnpm exec vitest run test/posts.test.ts

# watch the file you are editing
pnpm exec vitest test/posts.test.ts

Nutzen Sie schließlich Unit-Tests für die reine Logik und überlassen Sie den HTTP-Tests die Integration. Eine Suite, die jede einzelne Berechnung über eine vollständige Anfrage schickt, ist langsam, ohne dabei zusätzliche Sicherheit zu bieten.

Eine Test-Suite organisieren

Spiegeln Sie das Layout der Quelldateien wider, damit ein fehlgeschlagener Test auf eine Datei verweist, die Sie leicht finden können. Wenn die App src/routes/posts.ts hat, platzieren Sie test/posts.test.ts an der entsprechenden Stelle im Test-Baum.

Halten Sie gemeinsames Setup in einer geringen Anzahl von Helfern:

  • Ein test/app.ts, das die App mit der Test-Konfiguration aufbaut.
  • Ein test/db.ts, das die Datenbank migriert, leert und schließt.
  • Ein test/factories.ts mit Funktionen zum Erstellen von Benutzern, Posts und Tokens.
  • Ein test/tokens.ts, das einen Token mit dem Test-Secret signiert.
// test/factories.ts
export async function createUser(overrides: Partial<User> = {}) {
  return db.user.create({
    data: { email: "[email protected]", role: "member", ...overrides },
  });
}

Factories halten Tests lesbar, da der interessante Wert der Override ist. Ein Test, der createUser({ role: "admin" }) besagt, kommuniziert seine Absicht weitaus besser als eine Wand aus Literal-Feldern.

Tests unabhängig halten

Jeder Test sollte für sich allein und in beliebiger Reihenfolge bestehen. Diese Eigenschaft ermöglicht es einem Runner, Dateien zu parallelisieren, und verhindert, dass ein einzelner Fehler zu einem Dutzend irreführender Folgefehler führt.

Die Feinde der Unabhängigkeit sind gemeinsam genutzte, veränderbare Zustände (shared mutable state): ein Zähler auf Modulebene, ein vorbereiteter Datensatz, den ein anderer Test löscht, eine gemockte Uhr, die nie zurückgesetzt wird, oder eine Datenbank, die nur ein einziges Mal aufgesetzt wurde. Setzen Sie die Komponenten, von denen jeder Test abhängt, zurück, und verlassen Sie sich niemals darauf, dass ein vorheriger Test etwas erstellt hat.

Wenn eine Fixture tatsächlich ressourcenintensiv ist – wie eine migrierte Datenbank oder ein laufender Container –, erstellen Sie diese einmal in beforeAll und setzen Sie die veränderbaren Teile in beforeEach zurück. Die Unterscheidung liegt hier zwischen einem Setup, das schreibgeschützt ist, und einem Setup, das Änderungen vornimmt.

Best Practices

  • Exportiere die App aus app.ts und behalte listen() in server.ts.
  • Nutze await für jede Anfrage; ein fehlendes await führt zu einem stillen Fehler.
  • Überprüfe den Statuscode und den Content-Type, bevor du den Body validierst.
  • Teste auch die negativen Pfade — 400, 401, 403, 404, 422 — und nicht nur den Happy Path.
  • Signiere Test-Token mit einem Test-Secret, anstatt den echten Provider aufzurufen.
  • Setze die Daten zurück, die in einem Test verändert wurden, und schließe den Pool in afterAll.
  • Teste pro Testfall nur ein einziges Verhalten, damit ein Fehler genau benennt, was kaputtgegangen ist.
  • Nutze für Body-Assertions bevorzugt expect des Runners und .expect() für die Statuszeile.
  • Führe die Test-Suite gegen denselben middleware Stack aus, der auch in der Produktion verwendet wird.

Häufige Fehler

  • Der Aufruf von listen() innerhalb des importierten Moduls, wodurch jede Testdatei einen Port öffnet.
  • Das Vergessen von await, was dazu führt, dass ein Test besteht, noch bevor die Anfrage gesendet wurde.
  • Das Teilen einer Datenbankzeile zwischen Tests und die Abhängigkeit von der Ausführungsreihenfolge.
  • Das Testen des Frameworks — also die Überprüfung, ob Express JSON parst — anstatt des eigenen Codes.
  • Die Erwartung eines 200-Status, obwohl die Route einen 404 zurückgeben sollte.
  • Das Offenlassen des Datenbank-Pools, sodass der Prozess niemals beendet wird.
  • Ein zu starkes Mocking der Datenbank, sodass der Test nur beweist, dass der Mock funktioniert.
  • Die Überprüfung des Bodys mittels String-Match, obwohl ein struktureller Matcher klarer wäre.
  • Das Ignorieren von Headern wie Location, Set-Cookie und Cache-Direktiven.

Wie geht es weiter?

Supertest deckt die HTTP-Grenze ab und lässt sich ideal mit allen umliegenden Tools kombinieren. Lies den Guide zu Vitest, um mehr über den Test-Runner zu erfahren, mit dem diese Tests ausgeführt werden, oder Jest, falls dein Projekt bereits Jest nutzt. Der Express-Guide erklärt das App-Objekt, das du an Supertest übergibst, und der Abschnitt zu REST behandelt die Statuscodes und die Semantik, die du in deinen Assertions kodierst. Sobald die API abgedeckt ist, zeigt End-to-End Testing, wie du dieselben User-Journeys über einen echten Browser nachweisen kannst.

In der Praxis

Vom ersten Request zu echten Daten

Vier Tests, die die Struktur einer typischen API-Suite abdecken.

posts.test.ts
import request from "supertest";
import { expect, test } from "vitest";
import app from "../src/app.js";

test("GET /posts returns a list", async () => {
  const res = await request(app).get("/posts").expect(200);

  expect(res.headers["content-type"]).toMatch(/json/);
  expect(res.body).toHaveLength(3);
});

App importieren vs. Server starten

Supertest kann einen laufenden Server per URL testen, aber der Import der App hält den Test in einem Prozess und eliminiert Port- und Timing-Probleme.

Bevorzugt
import app from "../src/app.js";
import request from "supertest";

const res = await request(app).get("/health").expect(200);
Vermeiden
const server = app.listen(3000);

const res = await fetch("http://localhost:3000/health");
expect(res.status).toBe(200);

server.close();
// a fixed port collides in CI and the server
// may not be ready when fetch runs

Contract prüfen vs. Internals prüfen

Testen Sie die Response, die ein Client tatsächlich sieht. Der Zugriff auf die Datenbank oder private Helfer koppelt die Suite an Implementierungsdetails, die sich ändern können.

Bevorzugt
const res = await request(app)
  .post("/posts")
  .send({ title: "Hello" })
  .expect(201);

expect(res.body).toMatchObject({ title: "Hello" });
Vermeiden
await request(app)
  .post("/posts")
  .send({ title: "Hello" })
  .expect(201);

const [row] = await db.query("SELECT * FROM posts");
expect(row.title).toBe("Hello");
// breaks the moment the schema or query changes

Abwägungen

Wo Supertest aufhört

Supertest ist ein Request-Builder und Assertion-Helper, keine Teststrategie. Wissen Sie, was es bewusst Ihnen überlässt.

Strengths

  • Fast kein Setup

    Wenn Sie bereits ein App-Objekt und einen Test-Runner haben, ist ein einziger Import die gesamte Installation.

  • Schnell und hermetisch

    Tests laufen im selben Prozess mit einem temporären Port, sodass kein externer Dienst gestartet werden muss und keine Netzwerk-Instabilitäten auftreten.

  • Liest sich wie der Request

    Die Chain spiegelt HTTP wider, was Fehler leicht lesbar und neue Tests schnell zu schreiben macht.

Trade-offs

  • Kein State-Management

    Supertest hat keine Fixtures oder Transaktionen. Die Datenbank zwischen Tests sauber zu halten, liegt vollständig in Ihrer Verantwortung.

  • Keine Rendering-Bugs

    Alles unterhalb der HTTP-Grenze ist unsichtbar. Eine 200-Response sagt nichts darüber aus, ob die UI diese nutzen kann.

  • Kein Browser

    Es wird kein JavaScript ausgeführt, Cookies werden nicht wie im Browser erzwungen, und Redirects sowie CORS verhalten sich anders.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, Supertest zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch Supertest — mit Quizzen und echtem Code, den Sie im Browser ausführen können.