Chapter 19: OpenAPI Mocks — Auto-Generate HTTP Stubs From Your Contract
Duration: 15 minutes Prerequisites: Chapter 17 (Mock Services), an OpenAPI 3.0 document (YAML or JSON)
Goals & purpose
Your SUT talks to five HTTP upstreams. Each has an OpenAPI spec. You
want to mock all five in tests, and you don’t want to hand-copy every
path/method/response into a routes={} dict that will drift from the
real spec the moment someone ships a new endpoint.
OpenAPI mocks (shipped in v0.9.3) point Faultbox at the spec file.
Routes auto-generate from the paths: tree. Responses come from the
example: declared in each operation. Overrides let you swap specific
responses when you need custom behaviour. Request validation fails the
SUT fast when it sends a malformed body.
This chapter teaches you to:
- Load an OpenAPI document into a mock via
http.server(openapi=…). - Pick response examples by strategy —
first, named, random, or schema synthesis. - Override specific operations with custom responses or dynamic handlers.
- Enforce request schemas with
validate="strict"so the SUT can’t send a malformed body and have it silently accepted.
1 · The minimum working mock
Start with a Petstore-ish spec:
# specs/petstore.yaml
openapi: 3.0.3
info: {title: Petstore, version: "1.0"}
paths:
/pets:
get:
responses:
"200":
description: list pets
content:
application/json:
example:
- {id: 1, name: "fluffy"}
- {id: 2, name: "scruffy"}
/pets/{id}:
get:
parameters:
- {name: id, in: path, required: true, schema: {type: integer}}
responses:
"200":
description: single pet
content:
application/json:
example: {id: 1, name: "fluffy"}
Point Faultbox at it:
load("@faultbox/mocks/http.star", "http")
petstore = http.server(
name = "petstore",
interface = interface("main", "http", 8090),
openapi = "./specs/petstore.yaml",
)
def test_list_pets():
result = petstore.main.get(path = "/pets")
assert_true(result.status == 200)
assert_true(result.body.startswith("[{"))
def test_get_pet():
result = petstore.main.get(path = "/pets/42")
assert_true(result.status == 200)
assert_true("fluffy" in result.body)
Two lines of Starlark, every example: in the spec becomes a live
response. No route table to maintain.
2 · Choosing which example to serve
Real specs rarely have one example per operation. Use examples= to
pick a strategy:
# Deterministic default — first inline example, else first entry in
# examples: map (sorted alphabetically). Same response every time.
petstore = http.server(
...,
examples = "first",
)
# Select a named variant across all ops. Falls back to "first" for ops
# that don't declare that variant — useful when your spec has mixed
# coverage.
error_ride = http.server(
...,
examples = "error",
)
# Seeded random per op — reproducible between runs, different examples
# per op. Good for exercising client code paths that branch on response
# shape.
fuzz_ride = http.server(
...,
examples = "random",
)
# Synthesise minimal type-correct values for ops that declare only a
# schema (no example). Keeps the spec usable even when spec authors
# haven't filled in examples yet.
partial = http.server(
...,
examples = "synthesize",
)
3 · Overriding specific operations
Sometimes you want the generated routes for everything except two
critical ops. overrides={} replaces generated entries by pattern:
petstore = http.server(
name = "petstore",
interface = interface("main", "http", 8090),
openapi = "./specs/petstore.yaml",
overrides = {
# OpenAPI-style `{id}` — normalised to a glob internally, so
# you can paste paths directly from your spec.
"GET /pets/{id}": json_response(status = 404, body = {"error": "not found"}),
# Dynamic responses still work.
"POST /pets": dynamic(lambda req: json_response(
status = 201,
body = {"echoed": req["body"]},
)),
},
)
Overrides take priority over generated routes. Anything you don’t override is served from the spec.
4 · Validating requests
If your SUT sends a malformed POST body, the real upstream would
return a 400. The mock should too — otherwise bugs hide until
production. Add validate="strict":
auth = http.server(
name = "auth",
interface = interface("main", "http", 8090),
openapi = "./specs/auth.openapi.yaml",
validate = "strict", # reject malformed bodies with HTTP 400
)
def test_login_requires_password():
# Missing required "password" field — mock returns 400.
result = auth.main.post(path = "/login",
body = '{"email": "x@y.z"}')
assert_true(result.status == 400)
Alternatives:
validate="warn"— log the mismatch as an event (mock.POST /loginwith avalidate_errorfield) but serve the generated response anyway. Good for passively detecting contract drift in CI.validate="off"(default) — no validation.
Only JSON request bodies are validated in v0.9.3. Non-JSON content types pass through unchecked.
5 · Combining with faults
OpenAPI mocks are just mock_service instances — every other
Faultbox primitive works against them. Inject a protocol-level fault at
the mock’s interface to simulate a flapping upstream:
def test_auth_upstream_flake():
# Half the calls to /login get a 503 instead of the generated
# response - the mock's proxy layer rewrites them.
fault(auth.main,
error(path = "/login", status = 503, probability = 0.5),
run = lambda: my_app_handles_auth_flake(),
)
fault(iface, error(...), run=...) applies the rule for the duration
of run= and lifts it afterward. error(...) here is a protocol
fault - path=/status=/probability= - not the mock response
constructor of the same name.
Takeaways
http.server(openapi=…)auto-generates routes from an OpenAPI 3.0 document.- Example-selection strategies —
first,<name>,random,synthesize— let you pick what responses look like without rewriting the spec. overrides={}replaces generated routes by pattern. OpenAPI-style paths ({id}) work verbatim.validate="strict"makes the mock behave like a real OpenAPI-enforcing gateway.- Malformed spec files fail at
faultbox testload, not mid-run.