Wir verwenden Cookies

Wir versuchen unsere Website ständig zu verbessern und verwenden dafür Cookies. Was das genau bedeutet, kannst du in unserer Datenschutzerklärung lesen.

Mock Server mit API Recording für E2E Testing bei SSR

record replay

Auf einen Blick

Hast du dich schon mal gefragt, wie man serverseitige API-Requests mockt? Im Browser ist das easy. Bei Server-Side Rendering sieht dein Testbrowser die Calls aber gar nicht mehr. Und dann immer diese riesigen statischen Mock-Daten: Für kleine Code-Änderungen passt du hunderte Zeilen Mock-Daten an. Der smartive Mock Server zeichnet echte API-Responses auf und spielt sie deterministisch wieder ab. So bleiben deine E2E-Tests stabil auch wenn deine App serverseitig rendert.

Im Browser war alles noch einfach

Moderne Web-Apps hängen fast immer an externen APIs – Headless CMS, Auth-Services, Payment-Provider, Search, you name it. Für E2E-Tests heisst das: Du testest nicht nur deine App, sondern auch fremde Systeme. Und genau das macht Tests instabil und unzuverlässig.

Und was macht man mit externen Abhängigkeiten in Tests? Man isoliert sie. Test Isolation ist nichts Neues — externe Systeme werden weggemockt, damit Tests deterministisch bleiben.

In klassischen clientseitigen Apps war das trivial:

Browser → externe API

Mit Tools wie Playwright, MSW, Axios-Interceptors usw. lassen sich HTTP-Requests direkt im Browser abfangen und durch Mock-Responses ersetzen.

test("renders homepage headline", async ({ page }) => {
  await page.route(
    "https://api.storyblok.com/v2/cdn/stories/home*",
    async (route) => {
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          story: {
            slug: "home",
            content: {
              component: "page",
              heading: "Mocked Headline",
            },
          },
        }),
      });
    },
  );
  await page.goto("http://localhost:3000");
  await expect(page.getByText("Mocked Headline")).toBeVisible();
});

Wenn Server-Side Rendering ins Spiel kommt

Das Problem: Moderne Frameworks wie Next.js, Remix & Co. holen viele Daten serverseitig. Für SEO, Performance und Caching wandert der Datenfetch vom Browser auf den App-Server.

Die Architektur sieht dann so aus:

Browser → App Server → externe API

Der entscheidende Punkt: Der HTTP-Call läuft serverseitig. Der Testbrowser sieht ihn nie. Browser-Interceptors greifen nicht mehr, weil der Request ausserhalb der Browser-Runtime passiert. Test Isolation bleibt das Ziel – aber dein Mock sitzt an der falschen Stelle im System.

Die Lösung: Ein Mockserver

Die Idee ist simpel: Statt Requests abzufangen, lässt du sie ganz normal rausgehen — nur eben nicht zur echten API, sondern zu einem Mockserver. Die App macht weiterhin echte serverseitige HTTP-Requests. Nur zeigt die Base-URL in der Testumgebung nicht mehr auf die echte API z. B. https://api.storyblok.com, sondern auf deinen Mockserver: http://localhost:1080. Alles andere im produktiven Code bleibt unverändert. Der Mockserver steht komplett unter deiner Kontrolle. Du kannst ihn per HTTP konfigurieren und festlegen, wie er auf bestimmte Requests reagieren soll.

test("renders homepage headline", async ({ page }) => {
  await fetch("http://localhost:1080/mock", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      request: {
        match: "/api.storyblok.com/v2/cdn/stories/home.*",
      },
      response: {
        status: 200,
        body: {
          story: {
            slug: "home",
            content: {
              component: "page",
              heading: "Mocked Headline",
            },
          },
        },
      },
    }),
  });
  await page.goto("http://localhost:3000");
  await expect(page.getByText("Mocked Headline")).toBeVisible();
});

Das Problem klassischer Mockserver

Musstest du schon mal echte Headless-CMS-Responses mocken? Die sind meistens riesig. Relationen, nested Components, Rich-Text, Media-Assets, Renditions, i18n, Draft-/Published-States, SEO-Metadaten usw. Ein einzelner Page-Call kann mehrere hundert Zeilen JSON zurückliefern. Diese Mockdaten manuell zu pflegen wird schnell zum Albtraum:

  • API ändert sich → Mocks kaputt
  • Feld umbenannt → Tests failen
  • Content erweitert → JSON anpassen

Je grösser das Projekt, desto schlimmer wird es. Kurz: Statische Mocks skalieren nicht.

Unser Ansatz: Mockserver als Proxy mit Record & Replay

Bei smartive haben wir dafür einen eigenen kleinen Mockserver gebaut. Er lässt sich nicht nur per HTTP konfigurieren, sondern kann auch als Proxy zwischen deine App und die echte API geschaltet werden.

Zuerst startest du ihn lokal, als Docker-Container:

docker run -p 1080:1080 smartive/mockserver

Anschliessend musst du deiner App sagen, dass sie in der Testumgebung nicht mehr direkt die echte API aufruft, sondern den Mockserver. Der Mockserver erwartet den Original-Host der API als Teil der URL.

STORYBLOK_API_BASE_URL="http://localhost:1080/api.storyblok.com"

Damit werden Requests wie

https://api.storyblok.com/v2/cdn/stories/home

automatisch zu

http://localhost:1080/api.storyblok.com/v2/cdn/stories/home

Der Mockserver erkennt daraus den Ziel-Host und proxied korrekt weiter zur echten API. Die Architektur sieht dann so aus:

Browser → App Server → smartive Mockserver → externe API

Damit entstehen drei klar getrennte Phasen.

Phase 1 – Record

Recording-Modus aktivieren:

await fetch("http://localhost:1080/mock/recordings", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ active: true }),
});

Ab jetzt fungiert der Mockserver als Proxy: Er leitet Requests an die echte API weiter und gibt die Responses unverändert zurück – speichert aber alles mit.

Jetzt lässt du deine E2E-Tests einmal normal durchlaufen, damit alle relevanten Requests aufgezeichnet werden.

Phase 2 – Recordings exportieren

Nach dem initialen Record-Run exportierst du die gespeicherten Recordings:

const response = await fetch("http://localhost:1080/mock/recordings");
const recordings = await response.json();
writeFileSync(
  "test/e2e/recordings/home.json",
  JSON.stringify(recordings, null, 2),
);

Dieses JSON speicherst du im Repo und versionierst es.

Achtung: Je nach API können diese Files sehr gross werden. Bevor du sie committest, solltest du unbedingt prüfen, dass keine Secrets enthalten sind – z. B. API-Keys, Tokens oder andere sensible Header.

Phase 3 – Replay

Im Replay-Modus nutzt du den Mockserver wie einen klassischen Mockserver – nur dass die Responses aus echten, zuvor aufgezeichneten API-Calls stammen. Der Server speichert keinen Zustand. Du lädst ihm die Recordings per HTTP wieder hoch und definierst damit sein Verhalten neu. In der CI startest du ihn, konfigurierst ihn und fütterst ihn mit den versionierten JSON-Dateien aus dem Repo. Ab dann beantwortet er alle Requests rein lokal – ohne echte API.

import recordings from "../recordings/home.json";

test("renders homepage headline", async ({ page }) => {
  await fetch("http://localhost:1080/mock/recordings", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      active: false,
      recordings,
    }),
  });
  await page.goto("http://localhost:3000");
  await expect(page.getByText("Mock Server mit API Recording")).toBeVisible();
});

Bereit, stabile E2E-Tests zu haben?

Probier den Mockserver selbst aus — lokal oder in deiner CI in wenigen Sekunden als Docker Container startklar

Der Mockserver ist bewusst extrem simpel gehalten. Rund 300 Zeilen Code, keine Magie, keine versteckte Logik. Genau so viel wie nötig – nicht mehr.

Und vor allem: Du musst dich nicht mehr durch tausende Zeilen handgeschriebene Mock-JSONs wühlen, wenn sich deine API ändert. Einmal neu recorden, committen, fertig.

Wenn du willst, schau dir den Code an, probier ihn aus oder bau dir deine eigene kleine Variante davon. Das Konzept ist einfach. Und genau das macht es so mächtig.

👉 Zum Github Repo

Geschrieben von
Raphael Wirth

Technology|Februar 2026

Weitere Artikel