Self-host WireMock and Hurl on VPS for API tests
Run WireMock mocks and Hurl API tests on your VPS: keep stubs in git, execute suites in CI, and preserve reports after Docker rebuilds.
Two jobs wey dey share one repository
Self-hosted API mocking and testing na two different jobs, and if you treat dem as one, you fit waste one week. Mock server dey stand in for dependency wey you no fit call from CI: payment provider, partner API, rate limited upstream, or service wey another team never release. API test runner dey call your own endpoints for fixed order and check whether the responses correct. E dey carry values from one response enter the next request.
The two no overlap. Mock server no dey report pass or fail. Test runner no get opinion about wetin payment provider go return when dem decline card. Most teams wey already rent a box end up running one of each. Same Docker Compose file dey start both, and dem dey review both for the same pull request.
Why you go self-host API mocking and testing?
Your fixtures na production-style data. Request body for API test fit be real customer record wey person only change the name, or wey nobody change the name because nobody check am. Recorded stubs worse: proxy recording stores anything wey upstream actually return, so stub directory wey recording build go hold live tokens and customer email addresses until person read every file. For hosted service, that data become another person incident and your data disclosure.
The second reason na reachability. Service wey bind to private address no dey reachable from hosted runner, so test no fit run at all. Every workaround get cost. If you publish API to internet so you fit test am, you don remove the reason why e dey private. Tunnel or public staging copy na another system wey you must maintain, and staging copy dey drift from production between releases. Runner wey dey for the same private network fit call the service directly and no need any of those things. Na this practical reason dey behind self-hosted GitHub Actions runner.
Wetin self-hosted mock server you suppose run?
All of dem dey run as container for server wey you own. The important question na wetin each one treat as the source of truth. This one decide whether rebuilding the container go cost you nothing or go take your whole afternoon.
- WireMock dey keep every stub as JSON file for
mappings/directory, while large response bodies dey for__files/. The image nawiremock/wiremock, and the root directory inside the container na/home/wiremock. E also dey work as recording proxy. When files dey disk, the mock fit live for git like any other code. - Mockoon CLI dey keep one whole mock API inside one JSON data file. Install am with
npm install -g @mockoon/cliand start am withmockoon-cli start --data ./data-file.json, or run themockoon/cliimage with that file bind mounted. The desktop app dey edit the same file, so you fit design am for UI and still commit the result without wahala. - MockServer dey run from
mockserver/mockserverimage and dey listen on port 1080. Expectations dey enter through its own REST API. This one dey useful from test code, but e risky for deployment: expectation wey HTTP call create go disappear when the container restart. Use its JSON initialization file for stubs wey suppose remain permanent. - Prism dey build the mock from your OpenAPI document instead of separate stub files. Install am with
npm install -g @stoplight/prism-cli, then runprism mock openapi.yaml. For container, add-h 0.0.0.0because Prism dey bind to localhost by default. If you no add am, outside traffic no go reach the container. - Microcks na the bigger option: e get web UI wey fit import OpenAPI documents and Postman collections, then serve dem as mocks and run contract tests. Full installation need MongoDB and Keycloak, plus Kafka for async features. The all in one
microcks-uberimage bundle an in-memory MongoDB. Project documentation talk say e fit ephemeral use, so treat anything wey you create for that UI as disposable and keep the source artifacts for git.
Wich self-hosted API test runner you suppose run?
The work here na one sequence: authenticate, create an order, read am back, then assert say the state don change. E need one value wey you capture from one response and use for the next request. Tool wey no fit carry state between calls na health check, no be API test.
- Hurl dey run plain text files of HTTP requests from one binary. One
[Captures]section dey pull values from response, one[Asserts]section dey check dem, and--testdey turn am into test runner with summary and exit code. Version 8.0.1 na the current one as of August 2026. - Bruno CLI dey run one folder of
.brufiles. Install am withnpm install -g @usebruno/cli, then runbru run folder --env Local --reporter-junit results.xml. The collection format na text files inside directory by design, so the diffs easy to read during review. - Newman dey run Postman collections outside Postman:
npm install -g newman, thennewman run collection.json -r cli,junit --reporter-junit-export results.xml. The problem na the format. The collection na one exported JSON blob, so editing dey happen for Postman and the file for git na copy wey fit become stale. - Schemathesis na another kind of check. E dey read OpenAPI schema and generate cases wey try produce responses wey your schema talk say no suppose happen:
uvx schemathesis run https://your.api/openapi.json. E dey find crashes and contract violations, but e no know anything about your business rules. So e dey work beside scripted suite instead of replacing am. - Hoppscotch self hosted na the web UI option, and e require one Postgres instance. Understand this tradeoff before you install am: collections dey live for database, no be for your repository.
One wey you suppose avoid. Step CI still dey appear for tool roundups, and the YAML workflow format dey read well, but the repository last receive commit for August 2024. Program wey dey between your CI and your API no be good place for code wey nobody dey maintain.
Put the mock server behind the firewall
The setup wey dey below run WireMock as replacement for payment provider. If compose file format still new to you, Docker Compose on a VPS explain the lifecycle commands wey this section assume.
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-stoppedThe 127.0.0.1: prefix for the port na the important part. Plain 8080:8080 go publish the mock for every interface, including your public IP. E go still remain reachable even when ufw dey deny that port, because Docker dey write im own rules inside DOCKER iptables chain, and dem rules dey get evaluated before ufw's INPUT rules. Bind am to loopback address instead, or to private interface address. Then kernel no go accept connection from outside.
Your service under test go then point to the mock. When the service dey run for the same compose project, the mock base URL na http://mock-payments:8080, because compose dey resolve service names for im own network. When the service dey run on the host, na http://127.0.0.1:8080. Set this through environment variable, never put am for code. Otherwise, the test URL fit ship to production.
Put stubs for ./mocks/payments/mappings/, one JSON file for each stub.
{
"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 am, then check wetin actually load.
docker compose up -d --wait mock-payments
curl -fsS http://127.0.0.1:8080/__admin/mappings--wait go block until the container report say e healthy. This one work because the WireMock image ship with HEALTHCHECK against im /__admin/health endpoint. The mappings call go list every stub wey the server read. If stub wey you write no dey that list, e never load. Check say the file dey under mappings/, not inside the mounted root, and check say the JSON parse correctly.
When request enter and no stub match am, WireMock go answer 404 with body wey start with Request was not matched, then follow with diff against the closest stub wey e get. Read that diff before you change anything, because e name the exact field wey differ. Most times, na path wey get /v1/charge, while the stub talk say /v1/charges.
Write the test as a sequence with state carried between calls
Hurl files na plain text. Install the deb from the project's releases.
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.debA suite wey dey test your own API against the mock dey inside 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"The [Captures] block na wetin make this an API test instead of two unrelated requests. order_id dey read from the first response and interpolate into the URL of the second one. The assertion on charge_id na the main purpose of the whole exercise: e prove say your service call the payment provider and store wetin come back, and the value e compare against na the one you write inside the WireMock stub. One file now cover both sides of the flow.
hurl --test --variable base_url=http://127.0.0.1:3000 \
--report-junit reports/junit.xml \
--report-json reports/json \
tests/When the run pass, e print one line for each file and one summary.
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 msWhen e fail, e print error: Assert failure with the file and line number, then the value wey e get against the value wey e expect, and hurl exit non zero so CI go stop. If status read pending where you expect paid, your service no process the mock response. The next thing to check na the WireMock request journal for /__admin/requests, wey show whether the call reach the mock at all.
Trigger the suite from your own CI runner
If runner dey registered for the same box, workflow short. Runner na ordinary process for host, so docker and hurl must dey installed for that host. E no inherit anything from hosted 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() for archive step important. If you no put am, failed test run go skip the copy, so you go lose the exact report wey you wan read. The copy must also land outside workspace, because runner dey clean workspace before the next job, and the reports go disappear with am.
Results keep am, no be only the last run
One JUnit XML file for each commit answers one question: e pass? E no answer when endpoint start to slow down, because nothing dey read those files after you stop opening dem. For trend, append one row for each run to small database for the same box. One table wey hold commit SHA, the file name, the pass count, the fail count, and the duration dey enough. SQLite for production on a VPS na reasonable place to put am: na one file, e no need server process, and the complete history dey follow the backup wey you already dey take. Parse Hurl's --report-json output instead of the JUnit XML, because na the machine readable format between the two.
Wetin suppose survive container rebuild
Mock definitions and test suites na source code. Dem belong for repository beside the service wey dem describe, and you suppose change dem for the same pull request wey change endpoint. Stub wey person edit for web UI, or expectation wey person push go MockServer through REST API for runtime, dey only for that container memory or that tool database. Run docker compose down and e don disappear, then nobody go notice until test start pass for wrong reason. If una repositories dey run for una own hardware too, self-hosted git server go keep the fixtures and the service inside one trust boundary.
Now, the practical rules. Pin image tags, because latest fit change how your mock dey match requests without any change for your repository, and e hard well-well to connect that failure back to the cause. Mount stub directories as read only when the tool no need write to dem. Never put mock stubs inside named Docker volume, because the volume go become source of truth and the copy for git go silently become wrong.
One more rule, and e dey catch people. If you build stubs by recording real traffic through proxy, read every generated file before you commit am. Recording dey hold exactly wetin upstream send back, including bearer tokens and customer email addresses. Committing am go put that data permanently for your repository, because git dey keep deleted content for history.
FAQ
API mock server and API test runner dey differ how?
Mock server dey answer requests. E dey stand in for dependency wey you no fit call from CI, and e no dey ever report pass or fail. API test runner dey send requests go your own service, check the responses with assertions, carry values from one call enter the next one, and e go exit with non zero when assertion fail. Dem solve different problems, and normal setup dey run both together: runner dey call your service while your service dey call the mock.
I fit test internal API from hosted CI runner?
No, unless you expose am. Hosted runner dey outside your network, so e no fit reach service wey dey bound to private address. Your options na to publish the API, run a tunnel, or maintain public staging copy. Each option add another system wey fit fail or leak data. Runner wey dey for the same private network fit call the service directly. Na this be the main practical reason teams dey self-host this work.
Where mock stubs and API test suites suppose dey?
For git, beside the service wey dem describe. Tools wey store definitions as files, like WireMock's mappings/ directory, Mockoon's data file, Hurl files, and Bruno's .bru folder, give you code review and container rebuild wey no cost anything. Tools wey store definitions for database or web UI need backup plan and export step. Na the export people dey forget until container don already disappear.
Why my mock dey return 404 when the stub look correct?
WireMock dey serve stub only when request match exactly. Request wey no match go get 404 with body wey start with Request was not matched. After that, e go show diff against the closest stub, and the diff go name the field wey different. Common causes na trailing slash for path, Content-Type header wey stub require but your client no send, urlPath wey dem use where stub need urlPathPattern for variable segment, and body matcher wey no fit the payload. Check /__admin/requests first to confirm say request reach the mock at all.
I still need mocks if I get staging environment?
Yes, for 2 reasons. Staging copy of upstream wey you no control fit still go down and still rate-limit you, so your suite fit fail for reasons wey no get anything to do with your code. E also no fit produce the responses wey you most need to test, like declined card or gateway timeout. Mock fit return those responses whenever you ask, at local network speed. This one fit turn suite wey dey take minutes against sandbox into one wey dey take seconds. Keep staging for final check before release, and use mocks for CI.