Skip to main content

HTTP Mocking

TUnit.Mocks.Http provides MockHttpHandler — a drop-in HttpMessageHandler replacement for testing code that uses HttpClient.

dotnet add package TUnit.Mocks.Http --prerelease

Getting Started

using TUnit.Mocks;

[Test]
public async Task Fetches_Users_From_Api()
{
// Arrange — MockHttpClient is a real HttpClient with a .Handler property
using var client = Mock.HttpClient("https://example.com");
client.Handler.OnGet("/api/users").RespondWithJson("""[{"id": 1, "name": "Alice"}]""");

// Act
var response = await client.GetAsync("/api/users");
var body = await response.Content.ReadAsStringAsync();

// Assert
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
await Assert.That(body).Contains("Alice");
}

Creating a Client

Mock.HttpClient() returns a MockHttpClient — a subclass of HttpClient with a .Handler property for configuring setups and verifying calls:

// With base address (most common)
using var client = Mock.HttpClient("https://api.example.com");

// Without base address
using var client = Mock.HttpClient();
client.BaseAddress = new Uri("https://api.example.com");

// Just the handler (when you need more control)
var handler = Mock.HttpHandler();
using var client = handler.CreateClient("https://api.example.com");

MockHttpClient is an HttpClient — pass it anywhere HttpClient is expected. Use .Handler for all setup and verification:

Setting Up Responses

All setup is done through client.Handler (or directly on a MockHttpHandler if you created one with Mock.HttpHandler()).

By HTTP Method

client.Handler.OnGet("/api/users").RespondWithJson("""[{"id": 1}]""");
client.Handler.OnPost("/api/users").Respond(HttpStatusCode.Created);
client.Handler.OnPut("/api/users/1").Respond(HttpStatusCode.NoContent);
client.Handler.OnDelete("/api/users/1").Respond(HttpStatusCode.NoContent);

Any Request

client.Handler.OnAnyRequest().Respond(HttpStatusCode.OK);

Custom Matching

Use OnRequest with a fluent matcher for complex conditions:

// Match by path prefix
client.Handler.OnRequest(r => r.Method(HttpMethod.Get).PathStartsWith("/api/v2"))
.RespondWithJson("""{"version": 2}""");

// Match by regex
client.Handler.OnRequest(r => r.PathMatches(@"/api/users/\d+"))
.RespondWithJson("""{"id": 1, "name": "Alice"}""");

// Match by header
client.Handler.OnRequest(r => r.HasHeader("Authorization"))
.RespondWithJson("""{"authenticated": true}""");

client.Handler.OnRequest(r => r.Header("Authorization", "Bearer valid-token"))
.RespondWithJson("""{"user": "admin"}""");

// Match by body content
client.Handler.OnRequest(r => r.BodyContains("searchQuery"))
.RespondWithJson("""{"results": []}""");

// Custom predicate
client.Handler.OnRequest(r => r.Matching(msg => msg.RequestUri?.Port == 8080))
.Respond(HttpStatusCode.OK);

Request Matcher Reference

MethodMatches
.Method(HttpMethod)Specific HTTP method
.Path(string)Exact path
.PathStartsWith(string)Path prefix
.PathMatches(string)Regex pattern on path
.Header(name, value)Header with exact value
.HasHeader(name)Header present (any value)
.BodyContains(string)Request body contains text
.Matching(predicate)Custom Func<HttpRequestMessage, bool>

Response Configuration

Basic Responses

// Status code only
client.Handler.OnGet("/health").Respond(HttpStatusCode.OK);

// JSON body
client.Handler.OnGet("/api/data").RespondWithJson("""{"key": "value"}""");

// Plain text body
client.Handler.OnGet("/api/version").RespondWithString("1.0.0");

Response Builder

For more control, use the response builder:

client.Handler.OnGet("/api/data")
.Respond(HttpStatusCode.OK)
.WithJsonContent("""{"key": "value"}""")
.WithHeader("X-Request-Id", "abc123");

Dynamic Responses

Build responses based on the incoming request:

client.Handler.OnPost("/api/echo")
.Respond()
.WithFactory(request =>
{
var body = request.Content?.ReadAsStringAsync().Result ?? "";
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(body)
};
});

Simulating Delays

client.Handler.OnGet("/api/slow")
.Respond(HttpStatusCode.OK)
.WithDelay(TimeSpan.FromSeconds(2));

Throwing Exceptions

client.Handler.OnGet("/api/failing")
.Throws("Connection refused");

client.Handler.OnGet("/api/timeout")
.Throws(new TaskCanceledException("Request timed out"));

Sequential Responses

Return different responses for successive requests to the same endpoint:

var setup = client.Handler.OnGet("/api/status");
setup.RespondWithString("starting");
setup.Then().RespondWithString("running");
setup.Then().RespondWithString("complete");

// 1st call: "starting"
// 2nd call: "running"
// 3rd+ calls: "complete" (last response repeats)

Unmatched Requests

By default, unmatched requests return 404 Not Found. You can change this:

// Change default status code
client.Handler.WithDefaultStatus(HttpStatusCode.ServiceUnavailable);

// Or throw on unmatched requests
client.Handler.ThrowOnUnmatched();

Verification

Verify Call Count

client.Handler.Verify(r => r.Method(HttpMethod.Get).Path("/api/users"), Times.Once);
client.Handler.Verify(r => r.Method(HttpMethod.Delete), Times.Never);

Verify No Unmatched Requests

client.Handler.VerifyNoUnmatchedRequests();

Inspect Captured Requests

await Assert.That(client.Handler.Requests).HasCount().EqualTo(2);
await Assert.That(client.Handler.Requests[0].Method).IsEqualTo(HttpMethod.Get);
await Assert.That(client.Handler.Requests[0].RequestUri!.PathAndQuery).IsEqualTo("/api/users");

// Check for unmatched requests
await Assert.That(client.Handler.UnmatchedRequests).HasCount().EqualTo(0);

Each CapturedRequest provides:

PropertyDescription
MethodHTTP method
RequestUriFull request URI
BodyRequest body as string (or null)
HeadersRequest headers
MatchedWhether a setup matched this request
TimestampWhen the request was captured

Reset

client.Handler.Reset(); // clears all setups and captured requests