http_server_mock
A WireMock-style HTTP mock server for Gleam integration tests. Start a real HTTP server in your test, describe how it should respond with plain Gleam functions, make requests against it, and inspect what it received.
This package is the core API: a single public module, http_server_mock.
You also need a runtime package for your target:
# Erlang target
gleam add --dev http_server_mock http_server_mock_erlang
# JavaScript target
gleam add --dev http_server_mock http_server_mock_js
Quick start
import gleam/http
import gleam/http/request
import gleam/http/response
import gleam/list
import http_server_mock
import http_server_mock_erlang
pub fn weather_api_test() {
use server <- http_server_mock.with_handler(
http_server_mock.new(http_server_mock_erlang.server()),
fn(req) {
case req.method, request.path_segments(req), request.get_query(req) {
http.Get, ["weather"], Ok([#("city", "Oslo")]) ->
response.new(200)
|> response.set_body("{\"temp\": 12, \"unit\": \"C\"}")
|> Ok
_, _, _ -> Error(http_server_mock.UnexpectedRequest(req))
}
},
)
// Point your code under test at the mock server.
let base_url = http_server_mock.base_url(server)
let result = my_weather_client.fetch(base_url, "Oslo")
let assert Ok(weather) = result
assert weather.temp == 12
assert list.length(http_server_mock.received(server)) == 1
// The server stops right here, as `weather_api_test` returns - not before.
}
Everything after the use line is the callback with_handler runs while
the server is up: the request, the assertions, all of it. The server is
only stopped once that callback returns, so there’s no explicit
start/stop bookkeeping needed for the common case.
Building stubs
stub pairs a matches predicate with a fixed Response(String) to send
back, both plain Gleam using gleam/http/request and gleam/http/response
directly. with_stubs takes a list of them, so a test can cover several
independent routes without one big case expression:
pub fn ping_and_greet_test() {
let ping =
http_server_mock.stub(
fn(req) { req.method == http.Get && req.path == "/ping" },
response.new(200) |> response.set_body("pong"),
)
let greet =
http_server_mock.stub(
fn(req) { req.method == http.Post && req.path == "/greet" },
response.new(200) |> response.set_body("hello!"),
)
use server <- http_server_mock.with_stubs(config, [ping, greet])
let base_url = http_server_mock.base_url(server)
let ping_response = my_http_client.get(base_url <> "/ping")
assert ping_response.body == "pong"
let greet_response = my_http_client.post(base_url <> "/greet", "{}")
assert greet_response.body == "hello!"
}
A stub’s response isn’t computed from the request: the test that builds it
already controls every value that ends up in whatever request it’s matching,
so it already has everything it needs to build the response too. Want a
different response for a different input? Register another stub with a
narrower matches predicate rather than branching inside one. (The one
thing this can’t do, echoing back data whose exact shape only the code
under test knows and not the test itself, like verifying an HTTP client’s
own serialization round-trips correctly, is what with_handler’s handler
is for, since it already has to be a function of the request to do its own
routing.)
Use with_stubs (rather than with_handler) when you have more than one
route: a single with_handler handler is a Gleam case expression, which
works well for one endpoint but gets unwieldy for a handful of independent
routes with their own match logic.
matches is a plain function, so matching on a query parameter or the
request body is just pattern matching over the values gleam/http/request
already exposes:
let search =
http_server_mock.stub(
fn(req) {
case req.path, request.get_query(req) {
"/search", Ok([#("q", "gleam")]) -> True
_, _ -> False
}
},
response.new(200) |> response.set_body("found"),
)
There’s no built-in way to make the same endpoint answer differently across
successive calls (1.x’s “scenarios” feature). If you need that, model it
yourself: add_stub/remove_stub a replacement stub after the request
that should trigger the transition, or wrap state in a closure your
matches/response construction can read. This may come back as a built-in
feature in a later release.
When more than one registered stub matches the same request, the one registered first wins: there’s no separate priority mechanism to reach for, just put the stub you want to win earlier in the list.
To remove a specific stub later, pass the exact Stub value back to
remove_stub:
let ping = http_server_mock.stub(matches, response)
use server <- http_server_mock.with_stubs(config, [ping])
// ...
http_server_mock.remove_stub(server, ping)
Inspecting what the server received
let requests = http_server_mock.received(server)
assert list.length(requests) == 1
// Requests that didn't match any stub (or that a `with_handler` handler
// had no case for): useful for diagnosing why an expected response never
// came back.
let unmatched = http_server_mock.unmatched_requests(server)
Both return plain List(Request(String)): write your own asserts with
whatever precision and failure message you want; the library doesn’t wrap
this in an assertion helper.
To check calls against one specific stub without re-writing its matches
predicate, pass the exact Stub value back to received_by:
let ping = http_server_mock.stub(matches, response)
use server <- http_server_mock.with_stubs(config, [ping])
// ...
assert list.length(http_server_mock.received_by(server, ping)) == 1
Resetting between test phases
http_server_mock.reset_stubs(server) // remove all stubs, keep request history
http_server_mock.reset_requests(server) // clear request history, keep stubs
http_server_mock.reset(server) // both at once
Escape hatch: manual start/stop
If your test can’t be structured as a use block (for example, gleeunit
setup/teardown pairs), use start/start_with_stubs and call stop
yourself:
let assert Ok(server) = http_server_mock.start_with_stubs(config, [my_stub])
// ...
http_server_mock.stop(server)
Known limitation: if the code between start and stop panics, stop
does not run: the server process is only cleaned up when the test runner
exits. with_stubs/with_handler have the same gap for a panicking test
body; this is a documented trade-off for 2.0.0, not a regression from
use.
Runtimes
| Package | Target | Underlying server |
|---|---|---|
http_server_mock_erlang | Erlang/OTP | mist + OTP actor |
http_server_mock_js | JavaScript | Node.js http in a Worker thread |
Pass the adapter from the runtime package to http_server_mock.new/1:
// Erlang
http_server_mock.new(http_server_mock_erlang.server())
// JavaScript
http_server_mock.new(http_server_mock_js.server())
Upgrading from 1.x
2.0.0 was a breaking rewrite: the matcher/response DSL, stub_builder, and
verify modules are gone, stubs are now plain fn(Request) -> Bool /
Response values over gleam/http, and there’s a single public module. See
the migration guide
for the full walkthrough.
License
MIT