Zelf API mocking en testen hosten op een VPS
Ontdek hoe u WireMock en Hurl combineert op uw eigen VPS. Voorkom foutmeldingen door API-mocks en testsuites te scheiden en beheer uw testdata volledig in eigen beheer.
Twee taken die één repository delen
Zelfgehoste API-mocking en het uitvoeren van tests zijn twee verschillende taken. Ze als één geheel behandelen kost onnodig veel tijd. Een mock-server fungeert als vervanger voor een afhankelijkheid die u niet vanuit CI kunt aanroepen: een betaalprovider, een partner-API, een upstream met rate limiting of een service die door een ander team nog niet is vrijgegeven. Een API-testrunner roept uw eigen endpoints in een vaste volgorde aan en valideert de antwoorden, waarbij waarden uit het ene antwoord worden doorgegeven aan het volgende verzoek.
De twee overlappen niet. Een mock-server rapporteert nooit of een test slaagt of faalt. Een testrunner heeft geen oordeel over wat een betaalprovider terugstuurt wanneer een kaart wordt geweigerd. De meeste teams die al een server huren, draaien uiteindelijk beide, gestart via hetzelfde Docker Compose-bestand en beoordeeld in dezelfde pull request.
Waarom API-mocking en -testen zelf hosten?
Uw fixtures bevatten data in de vorm van productiegegevens. Een request body in een API-test is een echt klantrecord met een gewijzigde naam, of met de oorspronkelijke naam omdat niemand dit heeft gecontroleerd. Opgenomen stubs zijn nog risicovoller: proxy-recording slaat alles op wat de upstream-service daadwerkelijk teruggeeft. Een map met stubs die via recording is opgebouwd, bevat dus live tokens en e-mailadressen van klanten totdat iemand elk bestand handmatig controleert. Op een gehoste dienst wordt die data het incident van iemand anders en uw openbaarmakingsplicht.
De tweede reden is bereikbaarheid. Een service die aan een privèadres is gebonden, is niet bereikbaar vanaf een gehoste runner, waardoor de test niet kan worden uitgevoerd. Elke workaround brengt kosten met zich mee. Het publiceren van de API op het internet om deze te testen, heft de reden voor het private karakter op. Een tunnel of een publieke staging-kopie is een extra systeem om te onderhouden, en een staging-kopie raakt tussen releases door uit de pas met de productieomgeving. Een runner op hetzelfde private netwerk roept de service rechtstreeks aan en heeft dit alles niet nodig; dit is het praktische argument achter een zelf-gehoste GitHub Actions runner.
Welke self-hosted mock-server moet u draaien?
Elk van deze opties draait als een container op een server die u beheert. De relevante vraag is wat elk systeem als bron van waarheid beschouwt, omdat dit bepaalt of het opnieuw opbouwen van de container u niets kost of een hele middag in beslag neemt.
- WireMock bewaart elke stub als een JSON-bestand in een
mappings/-directory, met grote response-bodies in__files/. De image iswiremock/wiremock, de root-directory binnen de container is/home/wiremock, en het programma fungeert ook als een opname-proxy. Omdat de bestanden op schijf staan, leeft de mock in git, net als elke andere code. - Mockoon CLI bewaart een volledige mock-API in één JSON-databestand. Installeer het met
npm install -g @mockoon/clien start het metmockoon-cli start --data ./data-file.json, of draai demockoon/cli-image met dat bestand via een bind mount gekoppeld. De desktop-app bewerkt hetzelfde bestand, waardoor ontwerpen in een UI en het committen van het resultaat compatibel blijven. - MockServer draait vanuit de
mockserver/mockserver-image en luistert op poort 1080. Verwachtingen (expectations) worden via de eigen REST API doorgegeven, wat handig is vanuit testcode maar riskant als deployment: een verwachting die via een HTTP-aanroep is aangemaakt, verdwijnt zodra de container herstart. Gebruik het JSON-initialisatiebestand voor stubs die permanent moeten zijn. - Prism bouwt de mock op basis van uw OpenAPI-document in plaats van losse stub-bestanden. Installeer het met
npm install -g @stoplight/prism-clien voer daarnaprism mock openapi.yamluit. Voeg binnen een container-h 0.0.0.0toe, omdat Prism standaard aan localhost bindt en anders onbereikbaar is van buiten de container. - Microcks is de uitgebreide optie: een web-UI die OpenAPI-documenten en Postman-collecties importeert, deze vervolgens als mocks serveert en contracttests uitvoert. Een volledige installatie vereist MongoDB en Keycloak, plus Kafka voor de asynchrone functies. De all-in-one
microcks-uber-image bundelt een in-memory MongoDB, waarvan het project documenteert dat deze geschikt is voor tijdelijk gebruik. Behandel alles wat in die UI wordt aangemaakt daarom als vervangbaar en bewaar de bronbestanden in git.
Welke self-hosted API-testrunner moet u gebruiken?
De taak bestaat hier uit een reeks: authenticeren, een order aanmaken, deze uitlezen en verifiëren of de status is gewijzigd. Hiervoor moet een waarde uit de ene response worden opgeslagen en gebruikt in het volgende verzoek. Een tool die geen status kan vasthouden tussen aanroepen door, is een health check, geen API-test.
- Hurl voert platte tekstbestanden met HTTP-verzoeken uit vanuit één binary. Een
[Captures]-sectie haalt waarden uit een response, een[Asserts]-sectie controleert deze, en--testmaakt er een testrunner van met een samenvatting en een exitcode. Versie 8.0.1 is de huidige versie per augustus 2026. - Bruno CLI voert een map met
.bru-bestanden uit. Installeer metnpm install -g @usebruno/clien voer vervolgensbru run folder --env Local --reporter-junit results.xmluit. Het collectieformaat bestaat standaard uit tekstbestanden in een map, waardoor de diffs leesbaar zijn tijdens een review. - Newman voert Postman-collecties uit buiten Postman:
npm install -g newman, gevolgd doornewman run collection.json -r cli,junit --reporter-junit-export results.xml. Het nadeel is het formaat. De collectie is één geëxporteerde JSON-blob; het bewerken gebeurt in Postman en het bestand in git is een kopie die verouderd raakt. - Schemathesis is een ander type controle. Het leest een OpenAPI-schema en genereert testgevallen die proberen responses te produceren die volgens uw schema onmogelijk zijn:
uvx schemathesis run https://your.api/openapi.json. Het vindt crashes en contractschendingen. Omdat het niets weet van uw bedrijfsregels, fungeert het als aanvulling op een gescripte suite in plaats van als vervanging. - Hoppscotch self-hosted is de optie met een web-UI en vereist een Postgres-instantie. Begrijp deze afweging voordat u het installeert: collecties bevinden zich in een database, niet in uw repository.
Eén om te vermijden. Step CI verschijnt nog steeds in tool-overzichten en het YAML-workflowformaat leest prettig, maar de repository heeft de laatste commit ontvangen in augustus 2024. Een programma dat zich tussen uw CI en uw API bevindt, is een ongeschikte plek voor niet-onderhouden code.
Plaats de mock-server achter de firewall
De onderstaande configuratie draait WireMock als vervanger voor een betalingsprovider. Als het formaat van het compose-bestand nieuw voor u is, behandelt Docker Compose op een VPS de lifecycle-commando's waarvan deze sectie uitgaat.
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-stoppedHet 127.0.0.1:-voorvoegsel bij de poort is het belangrijkste onderdeel. Een kale 8080:8080 publiceert de mock op elke interface, inclusief uw publieke IP-adres. Deze blijft bereikbaar, zelfs als ufw die poort blokkeert, omdat Docker zijn eigen regels schrijft naar de DOCKER iptables-chain. Deze worden geëvalueerd vóór de INPUT-regels van ufw. Bind in plaats daarvan aan het loopback-adres of aan een privaat interface-adres; de kernel accepteert dan geen verbindingen van buitenaf.
Uw service die wordt getest, verwijst vervolgens naar de mock. Wanneer de service in hetzelfde compose-project draait, is de basis-URL van de mock http://mock-payments:8080, omdat compose servicenamen op zijn eigen netwerk oplost. Wanneer de service op de host draait, is dit http://127.0.0.1:8080. Stel dit in via een omgevingsvariabele, nooit in de code, anders wordt de test-URL meegeleverd in de productieomgeving.
Stubs plaatst u in ./mocks/payments/mappings/, één JSON-bestand per stuk.
{
"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 }
}
}Start de container en controleer vervolgens wat er daadwerkelijk is geladen.
docker compose up -d --wait mock-payments
curl -fsS http://127.0.0.1:8080/__admin/mappings--wait wacht tot de container rapporteert dat deze 'healthy' is. Dit werkt omdat de WireMock-image een HEALTHCHECK bevat voor het /__admin/health-eindpunt. De mappings-aanroep toont elke stub die de server heeft ingelezen. Een stub die u heeft geschreven maar die ontbreekt in die lijst, is niet geladen: controleer of het bestand zich onder mappings/ bevindt in plaats van in de gemounte root, en controleer of de JSON correct geparseerd kan worden.
Wanneer er een verzoek binnenkomt en er geen stub overeenkomt, antwoordt WireMock met 404 met een body die begint met Request was not matched, gevolgd door een diff ten opzichte van de meest overeenkomstige stub die aanwezig is. Lees die diff voordat u wijzigingen aanbrengt, omdat deze het exacte veld benoemt dat afwijkt. Meestal is dit een pad met /v1/charge waar de stub /v1/charges aangeeft.
Schrijf de test als een reeks waarbij de status tussen aanroepen wordt behouden
Hurl-bestanden zijn platte tekst. Installeer het deb-pakket vanuit de releases van het project.
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.debEen suite die uw eigen API test tegen de mock bevindt zich 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"Het [Captures]-blok maakt dit een API-test in plaats van twee losstaande verzoeken. order_id wordt uitgelezen uit het eerste antwoord en geïnterpoleerd in de URL van het tweede. De assertie op charge_id is het doel van de hele oefening: het bewijst dat uw service de betalingsprovider heeft aangeroepen en de ontvangen gegevens heeft opgeslagen, en de waarde waarmee wordt vergeleken is de waarde die u in de WireMock-stub heeft geschreven. Eén bestand dekt nu beide helften van de flow.
hurl --test --variable base_url=http://127.0.0.1:3000 \
--report-junit reports/junit.xml \
--report-json reports/json \
tests/Een geslaagde run print één regel per bestand en een samenvatting.
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 msEen mislukking print error: Assert failure met het bestand en het regelnummer, gevolgd door de verkregen waarde versus de verwachte waarde, en hurl sluit af met een non-zero exitcode zodat CI stopt. Als status de waarde pending leest waar u paid verwachtte, heeft uw service het antwoord van de mock niet verwerkt. De volgende stap is het uitlezen van het WireMock-verzoekjournaal op /__admin/requests, dat laat zien of de aanroep de mock überhaupt heeft bereikt.
De suite aansturen vanaf uw eigen CI-runner
Met een runner geregistreerd op dezelfde machine is de workflow kort. De runner is een standaardproces op de host, dus docker en hurl moeten op die host zijn geïnstalleerd. Er wordt niets overgenomen van een gehoste image.
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() is van belang bij de archiveringsstap. Zonder deze vlag slaat een mislukte testrun het kopiëren over, waardoor u precies het rapport verliest dat u wilde inzien. De kopie moet bovendien buiten de workspace terechtkomen, omdat de runner de workspace opschoont voor de volgende taak en de rapporten daarmee verloren gaan.
Bewaar de resultaten, niet alleen de laatste run
Een JUnit XML-bestand per commit beantwoordt één vraag: is de test geslaagd. Het beantwoordt niet wanneer een endpoint trager begon te worden, omdat niemand die bestanden leest zodra u ze niet meer opent. Voeg voor een trend per run één rij toe aan een kleine database op dezelfde server. Eén tabel met de commit SHA, de bestandsnaam, het aantal geslaagde tests, het aantal mislukte tests en de duur is voldoende, en SQLite in productie op een VPS is een geschikte plek hiervoor: één bestand, geen serverproces, en de volledige geschiedenis wordt meegenomen in de back-up die u al maakt. Parse de --report-json-output van Hurl in plaats van de JUnit XML, aangezien dit van de twee het formaat is dat door machines gelezen kan worden.
Wat moet een container-rebuild overleven
Mock-definities en testsuites zijn broncode. Ze horen thuis in een repository naast de service die ze beschrijven, en worden gewijzigd in dezelfde pull request als de wijziging aan een endpoint. Een stub die in een web-UI wordt bewerkt, of een verwachting die tijdens runtime via de REST API naar MockServer wordt gepusht, bestaat alleen in het geheugen van die container of de database van die tool. Voer docker compose down uit en het is verdwenen, en niemand merkt het totdat een test om de verkeerde reden slaagt. Als uw repositories ook op uw eigen hardware draaien, houdt een zelfgehoste git-server de fixtures en de service binnen één vertrouwensgrens.
Dan de praktische regels. Pin image-tags vast, omdat latest de manier waarop uw mock verzoeken matcht kan veranderen zonder wijziging in uw repository, en die fout is zeer lastig te herleiden naar de oorzaak. Mount stub-mappen alleen-lezen wanneer de tool er niet naar hoeft te schrijven. Plaats de stubs van een mock nooit in een named Docker volume, omdat het volume dan de bron van waarheid wordt en de kopie in git ongemerkt onjuist raakt.
Nog één punt, en dit is een veelvoorkomende valkuil. Als u stubs bouwt door echt verkeer via een proxy op te nemen, lees dan elk gegenereerd bestand voordat u het commit. Een opname bevat exact wat de upstream terugstuurde, inclusief bearer tokens en e-mailadressen van klanten. Door dit te committen plaatst u deze gegevens permanent in uw repository, omdat git verwijderde inhoud in de geschiedenis bewaart.
FAQ
Wat is het verschil tussen een API mock server en een API test runner?
Een mock server beantwoordt verzoeken. Deze fungeert als vervanger voor een afhankelijkheid die u niet vanuit CI kunt aanroepen en rapporteert nooit of een test slaagt of faalt. Een API test runner stuurt verzoeken naar uw eigen service, valideert de antwoorden, draagt waarden over van de ene naar de volgende aanroep en sluit af met een non-zero exitcode wanneer een assertie faalt. Ze lossen verschillende problemen op en een standaardopstelling voert beide tegelijk uit: de runner roept uw service aan terwijl uw service de mock aanroept.
Kan ik een interne API testen vanaf een gehoste CI runner?
Niet zonder deze bloot te stellen. Een gehoste runner bevindt zich buiten uw netwerk en kan daarom geen service bereiken die aan een privaat adres is gebonden. Uw opties zijn het publiceren van de API, het draaien van een tunnel of het onderhouden van een publieke staging-omgeving; elk van deze opties voegt een systeem toe dat kan falen of lekken. Een runner op hetzelfde private netwerk roept de service direct aan, wat de voornaamste praktische reden is waarom teams dit werk zelf hosten.
Waar moeten mock stubs en API test suites worden opgeslagen?
In git, naast de service die ze beschrijven. Tools die definities als bestanden opslaan, zoals de mappings/-directory van WireMock, het databestand van Mockoon, Hurl-bestanden en de .bru-map van Bruno, bieden u de mogelijkheid tot code review en een container-rebuild die geen extra kosten met zich meebrengt. Tools die definities in een database of een web-UI opslaan, vereisen een back-upplan en een exportstap; de export is het onderdeel dat mensen vergeten totdat de container al is verwijderd.
Waarom geeft mijn mock een 404 terug terwijl de stub correct lijkt?
WireMock serveert een stub alleen bij een exacte match. Een niet-overeenkomend verzoek krijgt 404 met een body die begint met Request was not matched, gevolgd door een diff ten opzichte van de meest nabijgelegen stub; die diff benoemt het veld dat afwijkt. Veelvoorkomende oorzaken zijn een afsluitende slash in het pad, een Content-Type-header die de stub vereist maar die uw client niet heeft meegestuurd, urlPath gebruikt waar de stub urlPathPattern vereist voor een variabel segment, en een body-matcher die niet overeenkomt met de payload. Controleer eerst /__admin/requests om te bevestigen dat het verzoek de mock überhaupt heeft bereikt.
Heb ik nog steeds mocks nodig als ik een staging-omgeving heb?
Ja, om twee redenen. Een staging-kopie van een upstream waar u geen controle over heeft, gaat nog steeds offline en hanteert nog steeds rate limits, waardoor uw suite faalt om redenen die niets met uw code te maken hebben. Bovendien kan deze niet de antwoorden produceren die u het meest nodig heeft om te testen, zoals een geweigerde kaart of een gateway-timeout. Een mock retourneert deze op aanvraag op lokale netwerksnelheid, wat een suite die minuten duurt tegen een sandbox verandert in een suite die seconden duurt. Gebruik staging voor de laatste controle voor een release en gebruik mocks in CI.