API-Mocking und Tests selbst auf einem VPS hosten
Mock-Server und Test-Runner lösen verschiedene Aufgaben: WireMock-Fixtures in Git, Hurl-Suites in CI und Reports, die einen VPS-Neuaufbau überstehen.
Zwei Aufgaben, ein Repository
Self-hosted API-Mocking und API-Tests sind zwei unterschiedliche Aufgaben. Wenn Sie sie als eine Aufgabe behandeln, verlieren Sie eine Woche. Ein Mock-Server ersetzt eine Abhängigkeit, die Sie aus CI nicht aufrufen können: einen Zahlungsanbieter, eine Partner-API, einen Upstream mit Rate-Limit oder einen Dienst, den ein anderes Team noch nicht veröffentlicht hat. Ein API-Test-Runner ruft Ihre eigenen Endpunkte in einer festen Reihenfolge auf und prüft die Antworten. Dabei übernimmt er Werte aus einer Antwort in die nächste Anfrage.
Die beiden Aufgaben überschneiden sich nicht. Ein Mock-Server meldet niemals Erfolg oder Fehlschlag. Ein Test-Runner gibt nicht vor, was ein Zahlungsanbieter bei der Ablehnung einer Karte zurückgibt. Die meisten Teams, die bereits einen Server mieten, betreiben letztlich jeweils eine Instanz. Beide werden von derselben Docker-Compose-Datei gestartet und im selben Pull Request geprüft.
Warum API-Mocking und -Tests selbst hosten?
Ihre Fixtures enthalten produktionsnahe Daten. Ein Request-Body in einem API-Test ist ein echter Kundendatensatz, bei dem der Name geändert wurde – oder unverändert blieb, weil niemand ihn geprüft hat. Aufgezeichnete Stubs sind noch problematischer: Bei einer Proxy-Aufzeichnung wird gespeichert, was der Upstream tatsächlich zurückgegeben hat. Ein durch Aufzeichnung erstelltes Stub-Verzeichnis enthält daher Live-Tokens und Kunden-E-Mail-Adressen, bis jemand jede Datei prüft. Bei einem gehosteten Dienst werden diese Daten zum Sicherheitsvorfall und zur Offenlegung bei einem Dritten.
Der zweite Grund ist die Erreichbarkeit. Ein Dienst, der an eine private Adresse gebunden ist, kann von einem gehosteten Runner nicht erreicht werden. Der Test kann daher überhaupt nicht ausgeführt werden. Jede Umgehung verursacht Aufwand. Wenn Sie die API zum Testen im Internet veröffentlichen, entfällt der Grund, aus dem sie privat war. Ein Tunnel oder eine öffentliche Staging-Kopie ist ein weiteres zu wartendes System. Außerdem weicht eine Staging-Kopie zwischen Releases von der Produktion ab. Ein Runner im selben privaten Netzwerk ruft den Dienst direkt auf und benötigt nichts davon. Das ist das praktische Argument für einen selbst gehosteten GitHub Actions Runner.
Welchen Self-Hosted-Mockserver sollten Sie betreiben?
Jeder dieser Mockserver läuft als Container auf einem Server, den Sie selbst verwalten. Entscheidend ist, welche Komponente jeweils als maßgebliche Quelle gilt. Davon hängt ab, ob das Neuerstellen des Containers kostenlos ist oder einen Nachmittag kostet.
- WireMock speichert jeden Stub als JSON-Datei in einem
mappings/-Verzeichnis. Größere Antwortinhalte liegen in__files/. Das Image istwiremock/wiremock, sein Stammverzeichnis im Container ist/home/wiremock. WireMock kann außerdem als Recording-Proxy ausgeführt werden. Dateien auf dem Datenträger bedeuten, dass der Mock wie jeder andere Code in git verwaltet werden kann. - Mockoon CLI speichert eine vollständige Mock-API in einer einzelnen JSON-Datendatei. Installieren Sie es mit
npm install -g @mockoon/cliund starten Sie es mitmockoon-cli start --data ./data-file.json. Alternativ können Sie dasmockoon/cli-Image mit dieser Datei als Bind-Mount ausführen. Die Desktop-Anwendung bearbeitet dieselbe Datei. Dadurch bleiben die Entwicklung in einer grafischen Oberfläche und das Committen des Ergebnisses kompatibel. - MockServer läuft aus dem
mockserver/mockserver-Image und lauscht auf Port 1080. Expectations werden über die eigene REST-API übermittelt. Das ist aus Testcode praktisch, als Bereitstellungsmodell jedoch riskant: Eine per HTTP-Aufruf erstellte Expectation geht beim Neustart des Containers verloren. Verwenden Sie für dauerhafte Stubs die JSON-Initialisierungsdatei. - Prism erstellt den Mock aus Ihrem OpenAPI-Dokument und nicht aus separaten Stub-Dateien. Installieren Sie es mit
npm install -g @stoplight/prism-cliund führen Sie anschließendprism mock openapi.yamlaus. Fügen Sie in einem Container-h 0.0.0.0hinzu, da Prism standardmäßig an localhost gebunden ist und andernfalls von außerhalb des Containers nicht erreichbar ist. - Microcks ist die umfangreichste Option: eine Weboberfläche, die OpenAPI-Dokumente und Postman-Sammlungen importiert, daraus Mocks bereitstellt und Vertragstests ausführt. Eine vollständige Installation benötigt MongoDB und Keycloak sowie Kafka für asynchrone Funktionen. Das
microcks-uber-Image bündelt eine MongoDB im Arbeitsspeicher. Laut Projektdokumentation ist es für kurzlebige Verwendung geeignet. Behandeln Sie daher alle in dieser Oberfläche erstellten Inhalte als temporär und speichern Sie die Quelldateien in git.
Welchen selbst gehosteten API-Test-Runner sollten Sie einsetzen?
Die Aufgabe besteht hier aus einer Abfolge: authentifizieren, eine Bestellung anlegen, sie wieder abrufen und prüfen, ob sich der Status geändert hat. Dafür muss ein Wert aus einer Antwort erfasst und in der nächsten Anfrage verwendet werden. Ein Tool, das keinen Status zwischen Aufrufen übertragen kann, ist ein Health Check und kein API-Test.
- Hurl führt Klartextdateien mit HTTP-Anfragen über eine einzelne Binärdatei aus. Ein
[Captures]-Abschnitt liest Werte aus einer Antwort aus, ein[Asserts]-Abschnitt prüft sie, und--testmacht daraus einen Test-Runner mit Zusammenfassung und Exit-Code. Version 8.0.1 ist im August 2026 aktuell. - Bruno CLI führt ein Verzeichnis mit
.bru-Dateien aus. Installieren Sie es mitnpm install -g @usebruno/cliund starten Sie es anschließend mitbru run folder --env Local --reporter-junit results.xml. Das Sammlungsformat besteht absichtlich aus Textdateien in einem Verzeichnis. Dadurch bleiben Diffs bei der Prüfung gut lesbar. - Newman führt Postman-Sammlungen außerhalb von Postman aus:
npm install -g newmanund anschließendnewman run collection.json -r cli,junit --reporter-junit-export results.xml. Der Nachteil ist das Format. Die Sammlung besteht aus einem einzigen exportierten JSON-Objekt. Die Bearbeitung erfolgt daher in Postman, und die Datei in git ist eine Kopie, die veraltet. - Schemathesis ist eine andere Art von Prüfung. Das Tool liest ein OpenAPI-Schema und erzeugt Testfälle, die Antworten hervorzurufen versuchen, die laut Schema unmöglich sind:
uvx schemathesis run https://your.api/openapi.json. Es findet Abstürze und Vertragsverletzungen. Über Ihre Geschäftsregeln weiß es nichts. Daher ergänzt es eine skriptbasierte Testsuite, statt sie zu ersetzen. - Hoppscotch als Self-Hosted-Variante ist die Option mit Weboberfläche und erfordert eine Postgres-Instanz. Machen Sie sich diese Abwägung vor der Installation klar: Sammlungen werden in einer Datenbank gespeichert, nicht in Ihrem Repository.
Ein Tool sollten Sie vermeiden. Step CI erscheint weiterhin in Übersichten zu Tools, und sein YAML-Workflow-Format ist gut lesbar. Das Repository erhielt jedoch zuletzt im August 2024 einen Commit. Ein Programm zwischen Ihrer CI und Ihrer API ist ein ungeeigneter Ort für nicht mehr gepflegten Code.
Die Mock-Server-Instanz hinter der Firewall betreiben
Die folgende Konfiguration führt WireMock als Ersatz für einen Zahlungsanbieter aus. Wenn das Compose-Dateiformat für Sie neu ist, beschreibt Docker Compose auf einem VPS die Lebenszyklusbefehle, die dieser Abschnitt voraussetzt.
services:
mock-payments:
image: wiremock/wiremock:3.13.2
command: ["--verbose"]
volumes:
- ./mocks/payments:/home/wiremock
ports:
- "127.0.0.1:8080:8080"
restart: unless-stoppedDer Präfix 127.0.0.1: für den Port ist der entscheidende Teil. Ein einfaches 8080:8080 veröffentlicht den Mock auf jeder Schnittstelle, einschließlich Ihrer öffentlichen IP-Adresse. Er bleibt dann auch erreichbar, wenn ufw diesen Port blockiert, weil Docker eigene Regeln in die DOCKER-iptables-Kette schreibt und diese vor den INPUT-Regeln von ufw ausgewertet werden. Binden Sie den Dienst stattdessen an die Loopback-Adresse oder an die Adresse einer privaten Schnittstelle. Dann akzeptiert der Kernel die Verbindung von außerhalb nicht.
Ihr zu testender Dienst verweist anschließend auf den Mock. Wenn der Dienst im selben Compose-Projekt läuft, lautet die Basis-URL des Mocks http://mock-payments:8080, weil Compose Dienstnamen in seinem eigenen Netzwerk auflöst. Wenn der Dienst auf dem Host läuft, lautet sie http://127.0.0.1:8080. Setzen Sie diesen Wert über eine Umgebungsvariable, niemals im Code. Andernfalls gelangt die Test-URL in die Produktionsumgebung.
Die Stubs werden in ./mocks/payments/mappings/ abgelegt, jeweils eine JSON-Datei.
{
"request": {
"method": "POST",
"urlPath": "/v1/charges",
"bodyPatterns": [{ "matchesJsonPath": "$.amount" }]
},
"response": {
"status": 201,
"headers": { "Content-Type": "application/json" },
"jsonBody": { "id": "ch_test_001", "status": "succeeded", "amount": 4200 }
}
}Starten Sie den Dienst und prüfen Sie anschließend, was tatsächlich geladen wurde.
docker compose up -d --wait mock-payments
curl -fsS http://127.0.0.1:8080/__admin/mappings--wait wartet, bis der Container einen fehlerfreien Zustand meldet. Das funktioniert, weil das WireMock-Image einen HEALTHCHECK für seinen /__admin/health-Endpunkt bereitstellt. Der Aufruf mappings listet alle Stubs auf, die der Server eingelesen hat. Fehlt ein von Ihnen erstellter Stub in dieser Liste, wurde er nicht geladen. Prüfen Sie, ob die Datei unter mappings/ liegt und nicht im eingebundenen Stammverzeichnis. Prüfen Sie außerdem, ob das JSON gültig ist.
Wenn eine Anfrage eintrifft und kein Stub passt, antwortet WireMock mit 404 und einem Body, der mit Request was not matched beginnt. Danach folgt ein Diff zum ähnlichsten Stub, den der Server kennt. Lesen Sie dieses Diff, bevor Sie etwas ändern. Es nennt das genaue Feld, das abweicht. Meist handelt es sich um einen Pfad mit /v1/charge, während im Stub /v1/charges steht.
Schreiben Sie den Test als Sequenz mit Zustandsübergabe zwischen den Aufrufen
Hurl-Dateien sind einfache Textdateien. Installieren Sie das deb-Paket aus den Releases des Projekts.
VERSION=8.0.1
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VERSION/hurl_${VERSION}_amd64.deb
sudo apt update && sudo apt install ./hurl_${VERSION}_amd64.debEine Testsuite, die Ihre eigene API gegen den Mock testet, befindet sich in tests/checkout.hurl.
POST {{base_url}}/orders
Content-Type: application/json
{
"sku": "ssd-1tb",
"amount": 4200
}
HTTP 201
[Captures]
order_id: jsonpath "$['id']"
GET {{base_url}}/orders/{{order_id}}
HTTP 200
[Asserts]
jsonpath "$.status" == "paid"
jsonpath "$.charge_id" == "ch_test_001"Der Block [Captures] macht daraus einen API-Test und nicht zwei voneinander unabhängige Requests. order_id wird aus der ersten Antwort gelesen und in die URL des zweiten Requests eingesetzt. Die Assertion für charge_id ist der entscheidende Teil des gesamten Tests: Sie weist nach, dass Ihr Dienst den Zahlungsanbieter aufgerufen und die Antwort gespeichert hat. Der Vergleichswert stammt aus dem WireMock-Stub, den Sie zuvor erstellt haben. Damit deckt eine Datei nun beide Teile des Ablaufs ab.
hurl --test --variable base_url=http://127.0.0.1:3000 \
--report-junit reports/junit.xml \
--report-json reports/json \
tests/Bei einem erfolgreichen Lauf wird für jede Datei eine Zeile sowie eine Zusammenfassung ausgegeben.
tests/checkout.hurl: Success (2 request(s) in 61 ms)
Executed files: 1
Executed requests: 2 (30.1/s)
Succeeded files: 1 (100.0%)
Failed files: 0 (0.0%)
Duration: 64 msBei einem Fehler wird error: Assert failure mit der Datei und der Zeilennummer ausgegeben. Danach folgen der erhaltene und der erwartete Wert. hurl beendet den Prozess mit einem Fehlercode, sodass CI stoppt. Wenn status pending liest, obwohl Sie paid erwartet haben, hat Ihr Dienst die Antwort des Mocks nicht verarbeitet. Als Nächstes sollten Sie das WireMock-Request-Journal unter /__admin/requests prüfen. Dort sehen Sie, ob der Aufruf den Mock überhaupt erreicht hat.
Suite über Ihren eigenen CI-Runner auslösen
Mit einem Runner, der auf demselben System registriert ist, bleibt der Workflow kurz. Der Runner läuft als normaler Prozess auf dem Host. Daher müssen docker und hurl auf diesem Host installiert sein. Von einem gehosteten Image wird nichts übernommen.
name: api-tests
on: [push]
jobs:
hurl:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Start the mock
run: docker compose up -d --wait mock-payments
- name: Run the suite
run: hurl --test --variable base_url=http://127.0.0.1:3000 --report-junit reports/junit.xml tests/
- name: Archive the reports
if: always()
run: install -d /srv/api-tests/reports/$GITHUB_SHA && cp -r reports/. /srv/api-tests/reports/$GITHUB_SHA/
- name: Stop the mock
if: always()
run: docker compose downif: always() im Archivierungsschritt ist wichtig. Ohne diese Option wird die Kopie bei einem fehlgeschlagenen Testlauf übersprungen. Dadurch geht genau der Bericht verloren, den Sie prüfen wollten. Die Kopie muss außerdem außerhalb des Workspace erstellt werden, weil der Runner den Workspace vor dem nächsten Job bereinigt und dabei auch die Berichte entfernt.
Ergebnisse speichern, nicht nur den letzten Lauf
Eine JUnit-XML-Datei pro Commit beantwortet eine Frage: War der Lauf erfolgreich? Sie zeigt nicht, wann ein Endpunkt langsamer wurde, weil niemand diese Dateien auswertet, sobald Sie sie nicht mehr öffnen. Für einen Trend hängen Sie pro Lauf eine Zeile an eine kleine Datenbank auf demselben Server an. Eine einzelne Tabelle mit Commit-SHA, Dateiname, Anzahl erfolgreicher Tests, Anzahl fehlgeschlagener Tests und Dauer reicht aus. SQLite im Produktivbetrieb auf einem VPS ist dafür ein sinnvoller Speicherort: eine Datei, kein Serverprozess, und die gesamte Historie ist in dem Backup enthalten, das Sie ohnehin erstellen. Parsen Sie Hurls --report-json-Ausgabe statt der JUnit-XML, da sie das maschinenlesbare Format der beiden ist.
Was einen Container-Neuaufbau überstehen muss
Mock-Definitionen und Testsammlungen sind Quellcode. Sie gehören in ein Repository neben den beschriebenen Dienst. Sie werden im selben Pull Request geändert, in dem auch ein Endpunkt geändert wird. Ein Stub, der in einer Weboberfläche bearbeitet wurde, oder eine Erwartung, die zur Laufzeit über die REST API an MockServer übertragen wurde, existiert nur im Speicher dieses Containers oder in der Datenbank dieses Tools. Führen Sie docker compose down aus, ist sie verschwunden. Niemand bemerkt das, bis ein Test aus dem falschen Grund erfolgreich ist. Wenn Ihre Repositories auch auf eigener Hardware laufen, hält ein selbst gehosteter Git-Server Fixtures und Dienst innerhalb derselben Vertrauensgrenze.
Nun zu den praktischen Regeln. Fixieren Sie Image-Tags. latest kann ändern, wie Ihr Mock Anfragen abgleicht, ohne dass sich etwas in Ihrem Repository ändert. Dieser Fehler lässt sich nur sehr schwer auf seine Ursache zurückführen. Binden Sie Stub-Verzeichnisse schreibgeschützt ein, wenn das Tool nicht in sie schreiben muss. Legen Sie die Stubs eines Mocks niemals in einem benannten Docker-Volume ab. Das Volume wird sonst zur maßgeblichen Quelle, während die Kopie in Git unbemerkt veraltet.
Noch ein Punkt, der häufig übersehen wird. Wenn Sie Stubs erstellen, indem Sie echten Datenverkehr über einen Proxy aufzeichnen, lesen Sie jede generierte Datei, bevor Sie sie committen. Eine Aufzeichnung enthält exakt das, was das Upstream-System zurückgesendet hat, einschließlich Bearer-Tokens und Kunden-E-Mail-Adressen. Wenn Sie sie committen, bleiben diese Daten dauerhaft in Ihrem Repository, weil Git gelöschte Inhalte in der Historie behält.
FAQ
Was ist der Unterschied zwischen einem API-Mockserver und einem API-Test-Runner?
Ein Mockserver beantwortet Anfragen. Er ersetzt eine Abhängigkeit, die Sie aus CI nicht aufrufen können, und meldet niemals Erfolg oder Fehlschlag. Ein API-Test-Runner sendet Anfragen an Ihren eigenen Dienst, prüft die Antworten mit Assertions, übernimmt Werte aus einem Aufruf in den nächsten und beendet sich mit einem Fehlercode, wenn eine Assertion fehlschlägt. Beide Werkzeuge lösen unterschiedliche Probleme. In einer typischen Konfiguration laufen sie gleichzeitig: Der Runner ruft Ihren Dienst auf, während Ihr Dienst den Mock aufruft.
Kann ich eine interne API von einem gehosteten CI-Runner aus testen?
Nicht, ohne sie nach außen freizugeben. Ein gehosteter Runner befindet sich außerhalb Ihres Netzwerks und kann daher keinen Dienst erreichen, der an eine private Adresse gebunden ist. Sie können die API veröffentlichen, einen Tunnel einrichten oder eine öffentlich erreichbare Staging-Kopie betreiben. Jede dieser Möglichkeiten führt jedoch ein zusätzliches System ein, das ausfallen oder Daten offenlegen kann. Ein Runner im selben privaten Netzwerk ruft den Dienst direkt auf. Das ist der wichtigste praktische Grund, diese Arbeit selbst zu hosten.
Wo sollten Mock-Stubs und API-Testsuiten liegen?
In git, neben dem Dienst, den sie beschreiben. Werkzeuge, die Definitionen als Dateien speichern, etwa das mappings/-Verzeichnis von WireMock, die Datendatei von Mockoon, Hurl-Dateien und der .bru-Ordner von Bruno, ermöglichen Code-Reviews und einen Container-Neuaufbau ohne zusätzlichen Aufwand. Werkzeuge, die Definitionen in einer Datenbank oder einer Weboberfläche speichern, benötigen einen Backup-Plan und einen Exportschritt. Den Export vergessen viele, bis der Container bereits gelöscht ist.
Warum liefert mein Mock 404, obwohl der Stub korrekt aussieht?
WireMock stellt einen Stub nur bei einer exakten Übereinstimmung bereit. Eine nicht passende Anfrage erhält 404 mit einem Body, der mit Request was not matched beginnt. Danach folgt ein Vergleich mit dem ähnlichsten Stub. Dieser Vergleich nennt das abweichende Feld. Häufige Ursachen sind ein abschließender Schrägstrich im Pfad, ein Content-Type-Header, den der Stub voraussetzt, den Ihr Client aber nicht gesendet hat, urlPath, obwohl der Stub für ein variables Segment urlPathPattern benötigt, oder ein Body-Matcher, der nicht zur Nutzlast passt. Prüfen Sie zuerst /__admin/requests, um zu bestätigen, dass die Anfrage den Mock überhaupt erreicht hat.
Benötige ich weiterhin Mocks, wenn ich eine Staging-Umgebung habe?
Ja, aus zwei Gründen. Eine Staging-Kopie eines Upstream-Dienstes, den Sie nicht kontrollieren, kann weiterhin ausfallen oder Ihre Anfragen begrenzen. Dadurch schlägt Ihre Testsuite aus Gründen fehl, die nichts mit Ihrem Code zu tun haben. Außerdem kann die Staging-Kopie die für Tests wichtigsten Antworten nicht gezielt erzeugen, etwa eine abgelehnte Kartenzahlung oder ein Gateway-Timeout. Ein Mock liefert solche Antworten bei Bedarf mit der Geschwindigkeit des lokalen Netzwerks. Dadurch wird aus einer Testsuite, die gegen eine Sandbox Minuten dauert, eine Testsuite, die Sekunden benötigt. Verwenden Sie Staging für die abschließende Prüfung vor einem Release und Mocks in CI.