jevcrates.git / jev-mock

Chapter 21: jev-mock — a stunt double for Jev

jev-mock is a pretend System One endpoint for tests. It is a real HTTP/2 server with real TLS, listening on 127.0.0.1 behind a certificate authority it made up a moment ago, and it answers however the test tells it to. A client under test cannot tell it from the real thing, except that it is free, instant and never in a bad mood unless you ask it to be.

It knows nothing about Postgres, jev-http or any other client: anything that speaks HTTP/2 over TLS can be tested against it.

Three ways to play the part

flowchart LR
  C["client under test"] -->|"HTTP/2 + TLS"| M["MockJev"]
  M -->|"Reply"| C
  M --- R1["a closure:<br/>answer anything"]
  M --- R2["replay::Replay:<br/>answer from a fixture"]
  M --- R3["forward::Forward:<br/>ask the real API, record it"]
  R3 -->|"real key added here"| J(("api.typesafe.ai"))
  R3 -->|"fixture()"| F[("fixture JSON")]
  F --> R2
  1. From a closure. MockJev::start(|request| Reply::json(200, ...)). The closure sees each Recorded request (method, path, headers, body) and returns a Reply: a status, headers, a JSON body, and optionally a delay, a wait until N requests have arrived, or exact raw bytes.
  2. From a recording. replay::Replay answers from a fixture, keyed by the exact request bytes. A request the fixture lacks gets a 400 that names it (error_type fixture_miss), and assert_complete, also run when the last Replay handle drops, fails the test. A changed request can never reach the network, and can never pass.
  3. By recording. forward::Forward passes each request to a real endpoint with the real API key and records the answer. The fixture it builds (fixture()) then replays exactly what the endpoint said to exactly those bytes. The client under test talks to the mock with any key; the real one is added by Forward and never recorded. It needs a multi-threaded tokio runtime. exchange(body) sends and records a request built elsewhere, without the mock. postjevsql's tools/record-gates --send uses both.

Aside. The mock is also a witness. It counts TCP accepts, TLS connections, requests abandoned before an answer (the client reset the stream), the most requests in flight at once, and the client's HTTP/2 PINGs. It can send GOAWAY on every connection, advertise a SETTINGS_MAX_CONCURRENT_STREAMS limit, or stop acknowledging PINGs on the next N connections, so a test can check that a client's keep-alive really notices a dead path.

Try it

jev-http's integration tests are the shortest real use:

cargo test -p jev-http --test mock

In a test of your own, on a tokio runtime:

let mock = jev_mock::MockJev::start(|_| jev_mock::Reply::json(200, answer)).await;
// point the client at mock.endpoint() and trust mock.ca_file()
assert_eq!(mock.requests().len(), 1);

For the people who maintain it

In this folder

PathWhat
src/The code: Chapter 21½.
Cargo.tomlDependencies: hyper and hyper-util (server), tokio-rustls with ring, rcgen for the throwaway CA, tempfile for the CA file, serde_json, tokio with a multi-threaded runtime.
CLAUDE.mdInvariants for agents.

← Previous: Chapter 20¾: jev-http/tests · Up: jevcrates · Next: Chapter 21½: inside jev-mock/src →