http_server_mock

A WireMock-style HTTP mock server library for Gleam, running on both Erlang and JavaScript targets. Start a real HTTP server in your tests, register stubs that describe how it should respond, make real HTTP calls against it, then inspect recorded requests to verify what happened.

Installation

Add the core package and a runtime package for your target:

Erlang:

gleam add http_server_mock http_server_mock_erlang

JavaScript:

gleam add http_server_mock http_server_mock_js

Quick start

import gleam/http
import http_server_mock
import http_server_mock_erlang  // or http_server_mock_js
import http_server_mock/matcher
import http_server_mock/response
import http_server_mock/stub_builder
import http_server_mock/verify

pub fn my_test() {
  let server =
    http_server_mock.new(http_server_mock_erlang.server())
    |> http_server_mock.with_stub(
      stub_builder.new()
      |> stub_builder.matching(
        matcher.new()
        |> matcher.method(http.Get)
        |> matcher.path("/greet"),
      )
      |> stub_builder.responding_with(
        response.new()
        |> response.status(200)
        |> response.body("hello"),
      )
      |> stub_builder.build(),
    )
    |> http_server_mock.start()

  // Make real HTTP calls against the server
  let url = http_server_mock.base_url(server) <> "/greet"
  // ... your HTTP client call here ...

  verify.called(server, matcher.new() |> matcher.path("/greet"))

  http_server_mock.stop(server)
}

Server lifecycle

// Create — picks a random free port by default
let server = http_server_mock.new(adapter)

// Optionally pin a port
let server = http_server_mock.new(adapter) |> http_server_mock.with_port(8080)

// Start
let server = http_server_mock.start(server)

// Get the base URL for your HTTP client
let url = http_server_mock.base_url(server)  // "http://localhost:54321"

// Stop
let server = http_server_mock.stop(server)

Phantom types (NotStarted, Started, Stopped) enforce correct usage at compile time — passing a stopped server to add_stub or base_url is a type error.

Stubs

Registering stubs

// Panics on failure — use for chaining during setup
http_server_mock.with_stub(server, stub)

// Returns Result — use when you want to handle failure
http_server_mock.add_stub(server, stub)

// Remove by ID
http_server_mock.remove_stub(server, stub_id)

// Remove all
http_server_mock.reset_stubs(server)

Building a stub

stub_builder.new()
|> stub_builder.matching(request_matcher)
|> stub_builder.responding_with(response_definition)
|> stub_builder.with_id("my-stub")       // optional custom ID
|> stub_builder.with_priority(1)          // lower wins; default is 5
|> stub_builder.build()

Matchers

Start with matcher.new() (matches everything) and add constraints:

matcher.new()
|> matcher.method(http.Post)
|> matcher.path("/users")
|> matcher.path_contains("/users")                    // substring
|> matcher.path_matching(types.Prefix("/api/"))       // StringMatcher
|> matcher.query_param("page", types.Exactly("2"))
|> matcher.header("Authorization", types.Prefix("Bearer "))
|> matcher.body_json("{\"key\":\"value\"}")           // exact JSON body

Responses

response.new()                            // 200, no headers, no body
|> response.status(201)
|> response.body("plain text")
|> response.json_body("{\"id\":1}")       // sets Content-Type: application/json
|> response.header("X-Custom", "value")
|> response.delay(200)                    // milliseconds

response.ok()                             // shorthand for 200 with no body

Verification

Verify functions assert and return the matched recorded requests, or panic with a descriptive message.

verify.called(server, matcher)                  // at least once
verify.called_times(server, matcher, 3)         // exactly 3 times
verify.called_at_least(server, matcher, 2)      // at least 2 times
verify.never_called(server, matcher)            // zero times

Inspecting recorded requests

let assert Ok(requests) = http_server_mock.recorded_requests(server)
let assert Ok(unmatched) = http_server_mock.unmatched_requests(server)

http_server_mock.reset_requests(server)    // clear history
http_server_mock.reset(server)             // clear stubs + history

Scenarios (stateful stubs)

Scenarios let you model sequences of responses from the same endpoint.

let initial_stub =
  stub_builder.new()
  |> stub_builder.matching(matcher.new() |> matcher.path("/state"))
  |> stub_builder.responding_with(response.new() |> response.body("first"))
  |> stub_builder.in_scenario("my-scenario")
  |> stub_builder.when_state_is(types.ScenarioStarted)
  |> stub_builder.then_transition_to("second-call")
  |> stub_builder.build()

let second_stub =
  stub_builder.new()
  |> stub_builder.matching(matcher.new() |> matcher.path("/state"))
  |> stub_builder.responding_with(response.new() |> response.body("second"))
  |> stub_builder.in_scenario("my-scenario")
  |> stub_builder.when_state_is("second-call")
  |> stub_builder.build()

Runtimes

PackageTargetUnderlying server
http_server_mock_erlangErlang/OTPmist + OTP actor
http_server_mock_jsJavaScriptNode.js http module 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())

License

MIT

Search Document