New: Debug encrypted microservice traffic with Speedscale's eBPF collector Read the announcement

A developer edits a response card while requests and responses travel through a transparent proxy machine between mock boxes and a running application

From Handwritten Mocks to proxymock: The Complete Loop

Part 10 of 10 in the Getting Started with Mocks series. Previously: When Handwritten Mocks Stop Scaling.


Handwritten mocks are cheap one at a time. This series built enough of them to show how quickly that stops being true. Nine posts took one package notifier from a function returning "delayed" to a captured response from a real carrier. Along the way, we hand-authored canned successes, failure cases, a spy, a stateful fake, an HTTP server, response fixtures, and contract-drift tests in four languages.

That is a lot of code whose only job is to imitate other code. Every new endpoint adds more requests and responses to invent. Every failure mode needs another setup path. Every upstream API change leaves someone responsible for noticing that the fixtures are stale, correcting them, and keeping each team’s copy in sync. Before a test can say anything useful about the application, a developer has to reconstruct enough of the dependency by hand.

proxymock does not remove the need to decide what to test. It removes much of that reconstruction work. Record real traffic once. Turn the outbound calls into mocks. Replay the inbound calls as tests. Make the mocked dependency fail on command. Then edit a human-readable recorded response until the contract check complains. This capstone reruns the first eight lessons as one loop at a real network boundary.

🎯 Key Takeaways
  • proxymock records inbound requests as replayable tests and outbound requests as dependency mocks.
  • Fault injection makes slow, broken, and malformed responses deterministic without changing the recording.
  • Recorded traffic can act like a stub, a wire-level spy, and a regression fixture, but it does not replace every kind of test double.
  • An edited RRPair simulates contract drift; fresh traffic is still required to discover what a real dependency does today.

One application, two sides of the wire

This walkthrough uses the open source Speedscale mock-lab. It contains the same small application in Go, Java, Node.js, Python, Ruby, .NET, and C++. The application exposes its own HTTP API and calls a CNCF projects API downstream.

That gives proxymock two useful views of the same run:

flowchart LR
    P[proxymock replay] -->|recorded inbound request| A[Application]
    A -->|real outbound request| M[proxymock mock]
    M -->|recorded response or injected fault| A
    A -->|observed response| P

Traffic entering the application becomes a test. Traffic leaving it becomes a mock. The application between those two boundaries is real: routing, request construction, parsing, error handling, and state all execute normally.

Install and initialize proxymock, clone mock-lab, and choose a language directory. The examples below use Go, but the recording and the proxymock commands are not tied to Go. The mock-lab README lists the start command and any runtime-specific setup for every implementation. The fault-injection and contract-validation examples require proxymock v2.5.814 or newer.

1. Start with reality, then take control

Part 1 said that a mock buys control by giving up reality. Recording traffic lets you postpone that trade until after you have observed the real exchange.

From the Go directory, wrap the application in a recorder:

proxymock record -- go run .

In a second terminal, from the mock-lab root, drive the application through proxymock’s inbound port:

./lab/tests/run_tests.sh --recording

The resulting recording contains both directions. Requests to the application are stored under localhost; calls from the application to the dependency are stored under demo-api.trafficreplay.com. Each interaction is an RRPair: one request and one response in a Markdown file you can read without a specialized viewer.

The repository also includes a committed recording, so you can skip the live capture and complete the rest of the walkthrough without calling the hosted dependency.

2. Let the smallest mock answer

Part 2 used a function that returned one canned answer. At the wire, the equivalent is a recorded outbound RRPair: when the application asks the same HTTP question, the mock returns the recorded response.

Start the application behind the committed mock set:

cd go
proxymock mock \
  --in ../lab/proxymock/recording \
  --no-passthrough \
  -- go run .

--no-passthrough matters. A request without a matching mock receives a 404 instead of escaping to the real dependency. A green run therefore means the recording covered the outbound traffic; it does not mean an accidental network call rescued the test.

This mock is larger than the lambda from Part 2, and it proves more. The real HTTP client still builds the URL, sends the request, reads the response, and parses the body. Use the lambda for decision logic. Use the RRPair when the wire behavior is part of the question.

3. Make failure boring again

Part 3 made a carrier refuse, time out, and return nonsense. A recorded mock can do the same without maintaining separate copies of the response.

Restart the mock with a fault scoped to /v1/projects:

proxymock mock \
  --in ../lab/proxymock/recording \
  --no-passthrough \
  --fault '/v1/projects:status=503' \
  -- go run .

The recording remains unchanged. Change only the fault to ask a different question:

FaultWhat it tests
status=503Whether the application surfaces or swallows a dependency failure
latency=2sWhether the client has a deliberate timeout budget
body=corruptWhether malformed JSON becomes an error instead of an empty value
connection=stallWhether a client without a deadline can hang forever
connection=dropWhether the application notices a response cut off mid-body
status=503,rate=1/3Whether intermittent failure and retry behavior are deterministic

The proxymock CLI reference documents the complete mock and fault syntax.

The mock process does not decide whether the application passed. It creates the condition. A replay, integration test, or other driver asserts what the application did with it.

4. Use the recording as a wire-level spy

Part 4 needed to answer, “Did it actually send?” The hand-written spy appended messages to a list. A proxymock mock records the outbound calls it observes, including matches, misses, and passthroughs, under its results directory.

Open that run with proxymock inspect or proxymock web. You can see the method, URL, headers, request body, response, and match result that crossed the boundary. In mock-lab the action is a request to the projects API rather than an SMS, but the testing question is the same: did the application make the expected external request, and how many times?

This is a wire-level spy, not proof that a real provider delivered anything. It proves what left the application. Part 4’s warning still holds.

5. Assert behavior, not the transcript

Part 5 broke a test by changing retry timing that no customer could observe. Recorded traffic makes it tempting to repeat the mistake at a larger scale: assert every header, every request order, and every millisecond simply because the data is available.

Instead, from the mock-lab root, replay the recorded inbound behavior and compare the result:

proxymock replay \
  --in lab/proxymock/recording \
  --test-against http://localhost:8080 \
  --fail-if 'requests.result-match-pct < 100'

Standard replay compares response status and body. If the implementation changes its internal call order but returns the same result, this assertion survives. If the customer-visible response changes, it fails.

Add an interaction assertion only where the interaction is itself the behavior: for example, a payment submitted once or a notification not sent after a failed lookup. Do not turn the complete traffic log into an approved transcript of today’s implementation.

6. Keep calling the doubles what they are

Part 6 separated stubs, spies, fakes, mocks, and dummies. proxymock does not collapse those categories. It moves some of them to a network boundary:

What proxymock is doingTest-double role
Returning a recorded dependency responseStub-like behavior
Recording the outbound request for inspectionSpy-like behavior
Matching a request and enforcing the allowed interactionMock-server behavior
Replaying a captured request at the applicationTest driver, not a test double

The order flow in mock-lab still uses a working in-memory store so a later request can retrieve what an earlier request created. That is a fake. Replacing it with a sequence of canned responses would make the test less honest, not more sophisticated.

Use proxymock where the seam is a socket. Keep the small function stub or in-memory fake where that remains the clearest answer.

7. Exercise the client that production runs

Part 7 moved the mock from the function seam to a real HTTP server. proxymock occupies the same boundary as a standalone mock server, with responses taken from recorded traffic instead of typed into each test.

The client now has to get all of these right:

  • the host, path, method, query parameters, and headers;
  • proxy and TLS behavior for its language runtime;
  • status-code handling and response parsing;
  • timeouts and incomplete responses;
  • changing values such as access tokens and newly created IDs.

mock-lab’s shared recording includes an OAuth-and-order flow whose token and order ID change on every run. A scoped blueprint carries those freshly created values into later requests rather than wildcarding them. That preserves the relationship between calls instead of teaching the mock that every credential and resource ID is interchangeable.

This is what the function stub never proved. It is also why you should keep the function tests: enumerating decision logic through a network would be slower and harder to understand.

8. Make the recorded mock lie where you can see it

Part 8 ended with a difficult truth: a recording was real once, not forever. Fortunately, an RRPair is ordinary Markdown. You can make a copy of the known-good dependency traffic and deliberately age it.

cp -R lab/proxymock/recording lab/proxymock/recording-drifted
proxymock inspect --in lab/proxymock/recording-drifted

In the copied response, delete a required field such as maturity, or change a numeric count to a string. The request, response headers, and JSON body are all visible in the same file. Save it, then validate the copied dependency traffic against mock-lab’s OpenAPI contract:

proxymock validate \
  --spec lab/openapi.yaml \
  --in lab/proxymock/recording-drifted/demo-api.trafficreplay.com

The known-good recording conforms. The edited copy exits nonzero and identifies the missing field or type mismatch by JSON path. mock-lab also ships lab/vendor-capture with those changes already made if you want to see the result without editing a file. The OpenAPI guide covers generating and validating other contracts.

That closes the tutorial gap, not the reality gap. You have proved what the application and contract check do if the dependency changes. To discover whether the real dependency changed, capture fresh traffic or run a scheduled live contract check, then compare that evidence with the baseline. An offline fixture cannot tell you what happened after it was recorded.

Does proxymock replace unit-test mocks?

No. The package notifier’s function stub is still the fastest way to ask, “Given delayed, should I send a message?” An in-memory fake is still the clearest way to ask whether a second call sees the first write.

proxymock answers questions those doubles cannot: what did the application put on the wire, can the real client parse a recorded response, what happens when the connection drops, and does today’s behavior still match an earlier run? The tools overlap, but their useful boundaries are different.

Can an edited RRPair catch contract drift?

It catches your response to a contract change. That is valuable because you can make a missing field, changed type, or renamed property happen locally and repeatably. It does not prove the vendor made that change.

Use the edited copy on every commit to keep the failure behavior covered. Use fresh traffic on a schedule to keep the baseline connected to reality. Fast tests carry the coverage; current evidence keeps them honest.

The complete loop

The first mock in this series returned one word. The final loop records an entire boundary, but the decision rule has not changed:

  1. Start with the smallest substitute that answers the question.
  2. Record real traffic when the wire becomes part of the risk.
  3. Replay inbound behavior while mocking outbound dependencies.
  4. Inject one failure at a time and assert the observable result.
  5. Preserve the baseline, edit a copy, and make drift fail loudly.
  6. Refresh the evidence often enough that “recorded” still means something.

Control and reality are still a trade. proxymock makes the exchange visible, repeatable, and stored in files you can inspect. It does not remove the trade, which is exactly why the lessons from the other nine posts still matter.

The complete runnable application, recordings, fault-injection examples, and contract-drift fixture are in speedscale/mock-lab.

Stop writing API mocks by hand

proxymock records real traffic from your running app and replays it as mocks — HTTP, gRPC, Postgres, Kafka, and more. Install in 30 seconds, no account required.