Back in late 2025, I wrote about HttpPlaygroundServer v1.0—a lightweight local HTTP test server that helped me debug and inspect HTTP client calls without spinning up a real backend. It logged requests, returned mock responses from files, and made it easier to see what my code was really sending over the wire.
Since then, the project has grown in a direction I didn’t fully anticipate at first. What started as a debugging tool has become something more: a way to do functional testing of HTTP client workflows—not just single calls, but entire sequences of calls that represent real behavior.
In this post, I want to share:
- How HttpPlaygroundServer evolved,
- What “functional testing” means in this context,
- Some real technical lessons I learned,
- And how you can inspect and verify call order in practice.
This is about sharing what I learned while building and using it.
From Debugging to Functional Testing
Version 1.0 of HttpPlaygroundServer was mainly about observability and simulating based on just files:
- Point your HTTP client to
localhost, - Capture every request into JSON files,
- Return responses from simple files,
- Inspect what actually happened.
That alone was incredibly useful. But as I started using it in more realistic scenarios, a pattern emerged: I wasn’t just interested in one request. I cared about workflows.
For example:
- Call my backend API to create something.
- Call a third-party API to enrich it.
- Depending on the response, update the original resource.
The correctness of my system depended not just on what was called, but in what order, under what conditions, and how many times. That’s when the idea of using HttpPlaygroundServer for functional testing really took shape.
Functional testing, as I’m using the term here, means:
- Running real HTTP calls,
- Exercising real client logic,
- And then verifying that the overall behavior matches expectations.
A “Mini” Functional Test
Here’s a simplified version of one of the examples in the repo. It tests a workflow where a client:
- Creates a cat,
- Tries to fetch a photo from another service,
- And may update the cat depending on whether a photo exists.
internal class FunctionalTestingWithServerSimulation
{
private static HttpPlaygoundServer _playgroundServer =
new(new MultiServerSimulator());
internal static async Task Run()
{
CancellationTokenSource cts = new();
ManualResetEventSlim serverStarted = new();
_playgroundServer.IsRequestLoggingEnabled = false;
_ = Task.Run(async () =>
await _playgroundServer.StartServer(serverStarted, cts.Token)
);
serverStarted.Wait();
bool result = await Test_Post_Cat_Valid_Id_No_Photo();
Console.WriteLine($"No photo scenario: {result}");
result = await Test_Post_Cat_Valid_Id_And_Photo();
Console.WriteLine($"With photo scenario: {result}");
cts.Cancel();
_playgroundServer.StopServer();
}
}
Each test clears previous data, runs a workflow, and then inspects what actually happened.
Inspecting and Verifying Call Order
HttpPlaygroundServer records every interaction as a pair:
List RequestResponses
They’re stored in the exact order they occurred, so you can verify behavior by simply walking the list.
Here’s a real example of verifying a POST -> GET -> PATCH workflow:
var calls = _playgroundServer.RequestResponses;
// Basic count check
if (calls.Count != 3)
{
Console.WriteLine($"Unexpected number of calls: {calls.Count}");
return false;
}
// Verify order and intent
bool isValid =
calls[0].Item1.Verb == HttpMethod.Post.Method &&
calls[0].Item1.URL.EndsWith(RequestSender.RestPath) &&
calls[1].Item1.Verb == HttpMethod.Get.Method &&
calls[1].Item1.URL.Contains(PhotoClient.RelativeURL) &&
calls[2].Item1.Verb == HttpMethod.Patch.Method &&
calls[2].Item1.URL.EndsWith(RequestSender.RestPath);
if (!isValid)
{
Console.WriteLine("Call sequence is incorrect");
for (int i = 0; i < calls.Count; i++)
{
Console.WriteLine($"{i}: {calls[i].Item1.Verb} {calls[i].Item1.URL}");
}
return false;
}
Console.WriteLine("Call sequence verified successfully");
return true;
This is the heart of functional testing here:
- Not mocking individual calls,
- Not asserting on tiny details in isolation,
- But verifying that the whole workflow behaves correctly.
Simulating Multiple Servers
To make this practical, I added the ability to plug in custom server logic. Here’s the idea behind a MultiServerSimulator:
- If the URL looks like a “cat” API, return cat responses.
- Otherwise, treat it as a “photo” API.
- Some responses come from files, some from simple in-memory logic.
This lets me simulate multiple backends with very little infrastructure. It’s not meant to be perfect—it’s meant to be good enough to test behavior.
Technical Lessons I Learned
1. You Can Only Read an HTTP Request Body Once
In .NET, an HttpRequest body is a stream. Once you read it, it’s gone—unless you explicitly buffer it.
Early on, I tried to:
- Read the body for logging,
- Then read it again for processing.
The second read returned nothing.
The fix was to buffer the request body early, then reuse that buffered content for both logging and processing. If you’re building any middleware or server tooling, this is an easy trap to fall into.
2. Stopping a Server Is Not the Same as Closing It
Calling “stop” doesn’t automatically mean:
- All sockets are closed,
- All loops exit,
- All tasks are done.
I had cases where the server looked stopped, but the process wouldn’t exit cleanly. The solution was to:
- Use cancellation tokens properly,
- Explicitly close listeners,
- And wait for background loops to finish.
If you’re writing any long-running server code, treat shutdown as a first-class feature—not an afterthought.
Where This Fits
HttpPlaygroundServer targets the gap between unit tests and full integration/E2E setups::
- Unit tests: fast, isolated, mocked.
- Integration tests/E2E tests: real services, real environments.
- Functional tests (business-logic workflows): real HTTP, simulated servers, real workflows.
For me, this layer has been incredibly valuable for:
- Verifying how my client code really behaves.
- Testing edge cases that are hard to reproduce in real systems.
Closing Thoughts
HttpPlaygroundServer started as a simple debugging aid. It became a way to test behavior, not just code. Along the way, it taught me things about streams, server lifecycles, and how subtle infrastructure details can affect correctness.
If you’re building tools, frameworks, or even just internal utilities, I’ve found that the most interesting lessons come from actually using what you build—and fixing it when it breaks in real ways.
You can find the source and samples here: https://github.com/sameerkapps/HttpPlayground
You can download the Nuget package here: https://www.nuget.org/packages/sameerk.HttpPlaygroundServer
And if you’re curious about the original version, the v1.0 blog is still up—it’s interesting to see how much the idea has changed since then.