A set of extension methods for mocking HttpClient and IHttpClientFactory with Moq.
Mocking HttpClient has historically been surprisingly difficult, with the solution being to either create a wrapper to mock instead (at the cost of cluttering the code) or use a separate HTTP library entirely. This package provides extension methods for Moq that make handling HTTP requests as easy as mocking a service method.
Install-Package Moq.Contrib.HttpClient
or dotnet add package Moq.Contrib.HttpClient
The library adds request/response variants of the standard Moq methods:
- Setup β SetupRequest, SetupAnyRequest
- SetupSequence β SetupRequestSequence, SetupAnyRequestSequence
- Verify β VerifyRequest, VerifyAnyRequest
- Returns(Async) β ReturnsResponse, ReturnsJsonResponse
All Setup and Verify helpers have the same overloads, abbreviated here:
SetupAnyRequest()
SetupRequest([HttpMethod method, ]Predicate<HttpRequestMessage> match)
SetupRequest(string|Uri requestUrl[, Predicate<HttpRequestMessage> match])
SetupRequest(HttpMethod method, string|Uri requestUrl[, Predicate<HttpRequestMessage> match])
requestUrl
matches the exact request URL, while the match
predicate allows
for more intricate matching, such as by query parameters or headers, and may be
async as well to inspect the request body.
The response helpers simplify sending a StringContent, JsonContent (using System.Text.Json), ByteArrayContent, StreamContent, or just a status code:
ReturnsResponse(HttpStatusCode statusCode[, HttpContent content], Action<HttpResponseMessage> configure = null)
ReturnsResponse([HttpStatusCode statusCode, ]string content, string mediaType = null, Encoding encoding = null, Action<HttpResponseMessage> configure = null))
ReturnsResponse([HttpStatusCode statusCode, ]byte[]|Stream content, string mediaType = null, Action<HttpResponseMessage> configure = null)
ReturnsJsonResponse<T>([HttpStatusCode statusCode, ]T value, JsonSerializerOptions options = null, Action<HttpResponseMessage> configure = null)
The statusCode
defaults to 200 OK if omitted, and the configure
action can
be used to set response headers.
// All requests made with HttpClient go through its handler's SendAsync() which we mock
var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var client = handler.CreateClient();
// A simple example that returns 404 for any request
handler.SetupAnyRequest()
.ReturnsResponse(HttpStatusCode.NotFound);
// Match GET requests to an endpoint that returns json (defaults to 200 OK)
handler.SetupRequest(HttpMethod.Get, "https://example.com/api/stuff")
.ReturnsJsonResponse(model);
// Setting additional headers on the response using the optional configure action
handler.SetupRequest(HttpMethod.Get, "https://example.com/api/stuff")
.ReturnsResponse(stream, configure: response =>
{
response.Content.Headers.LastModified = new DateTime(2022, 3, 9);
});
π‘ Why you should use MockBehavior.Strict for HttpClient
Consider the following:
handler.SetupRequest(HttpMethod.Get, "https://example.com/api/foos") .ReturnsJsonResponse(expected); List<Foo> actual = await foosService.GetFoos(); actual.Should().BeEquivalentTo(expected);This test fails unexpectedly with the following exception:
System.InvalidOperationException : Handler did not return a response message.
This is because Moq defaults to Loose mode which returns a default value if no setup matches, but HttpClient throws an InvalidOperationException if it receives null from the handler.
If we change it to MockBehavior.Strict:
- var handler = new Mock<HttpMessageHandler>(); + var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);We get a more useful exception that also includes the request that was made (here we see the URL was typo'd as "foo" instead of "foos"):
Moq.MockException : HttpMessageHandler.SendAsync(Method: GET, RequestUri: 'https://example.com/api/foo', Version: 1.1, Content: <null>, Headers: { }, System.Threading.CancellationToken) invocation failed with mock behavior Strict. All invocations on the mock must have a corresponding setup.
// The request helpers can take a predicate for more intricate request matching
handler.SetupRequest(r => r.Headers.Authorization?.Parameter != authToken)
.ReturnsResponse(HttpStatusCode.Unauthorized);
// The predicate can be async as well to inspect the request body
handler
.SetupRequest(HttpMethod.Post, url, async request =>
{
// This setup will only match calls with the expected id
var json = await request.Content.ReadFromJsonAsync<Model>();
return json.Id == expected.Id;
})
.ReturnsResponse(HttpStatusCode.Created);
// This is particularly useful for matching URLs with query parameters
handler
.SetupRequest(r =>
{
Url url = r.RequestUri;
return url.Path == baseUrl.AppendPathSegment("endpoint") &&
url.QueryParams["foo"].Equals("bar");
})
.ReturnsResponse("stuff");
The last example uses a URL builder library called Flurl to assist in checking the query string.
See "MatchesCustomPredicate" and "MatchesQueryParameters" in the request extension tests for further explanation, including various ways to inspect JSON requests.
Moq has two types of sequences:
SetupSequence()
which creates one setup that returns values in sequence, andInSequence().Setup()
which creates multiple setups underWhen()
conditions to ensure that they only match in order.
Both of these are supported; however, as with service methods, regular setups are generally most appropriate. The latter type can be useful, though, for cases where separate requests independent of each other (that is, not relying on information returned from the previous) must be made in a certain order.
See the sequence extensions tests for examples.
The normal Returns method can be used together with the request helpers for more complex responses:
handler.SetupRequest("https://example.com/hello")
.Returns(async (HttpRequestMessage request, CancellationToken _) => new HttpResponseMessage()
{
Content = new StringContent($"Hello, {await request.Content.ReadAsStringAsync()}")
});
var response = await client.PostAsync("https://example.com/hello", new StringContent("world"));
var body = await response.Content.ReadAsStringAsync(); // Hello, world
It's common to see HttpClient wrapped in a using
since it's IDisposable, but
this is, rather counterintuitively, incorrect and can lead to the application
eating up sockets. The standard advice is to reuse a single
HttpClient, yet this has the drawback of not responding to DNS changes.
ASP.NET Core introduces an IHttpClientFactory which "manages the pooling and lifetime of underlying HttpClientMessageHandler instances to avoid common DNS problems that occur when manually managing HttpClient lifetimes." As a bonus, it also makes HttpClient's ability to plug in middleware more accessible β for example, using Polly to automatically handle retries and failures.
If your classes simply receive an HttpClient injected via IHttpClientFactory, the tests don't need to do anything different. If the constructor takes the factory itself instead, this can be mocked the same way:
var handler = new Mock<HttpMessageHandler>();
var factory = handler.CreateClientFactory();
This factory can then be passed into the class or injected via
AutoMocker, and code calling factory.CreateClient()
will receive
clients backed by the mock handler.
The CreateClientFactory()
extension method returns a mock that's already set
up to return a default client. If you're using named clients, a setup can be
added like so:
// Configuring a named client (overriding the default)
Mock.Get(factory).Setup(x => x.CreateClient("api"))
.Returns(() =>
{
var client = handler.CreateClient();
client.BaseAddress = ApiBaseUrl;
return client;
});
Note: If you're getting a "Extension methods (here: HttpClientFactoryExtensions.CreateClient) may not be used in setup / verification expressions." error, make sure you're passing a string where it says
"api"
in the example.
For integration tests, rather than replace the IHttpClientFactory implementation in the service collection, it's possible to leverage the existing DI infrastructure and configure it to use a mock handler as the "primary" instead:
public class ExampleTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> factory;
private readonly Mock<HttpMessageHandler> githubHandler = new();
public ExampleTests(WebApplicationFactory<Startup> factory)
{
this.factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
// For the default (unnamed) client, use `Options.DefaultName`
services.AddHttpClient("github")
.ConfigurePrimaryHttpMessageHandler(() => githubHandler.Object);
});
});
}
This way, the integration tests use the same dependency injection and HttpClient
configurations from ConfigureServices()
(or Program.cs) as would be used in
production.
See this sample ASP.NET Core app and its integration test for a working example.
The library's own unit tests have been written to serve as examples of the various helpers and different use cases:
- Request extensions tests β these cover the Setup & Verify helpers and explain various ways of matching requests
- Response extensions tests β these cover the ReturnsResponse (and ReturnsJsonResponse) overloads
- Sequence extensions tests β these demonstrate mocking explicit sequences, as mentioned above
MIT