Mock HttpClient and IHttpClientFactory with Moq.Contrib.HttpClient

When writing unit tests for your ASP.NET applications, you may come across a scenario where you are making HTTP calls out to remote endpoints with .NET’s HttpClient. Also, it is quite possible that it is an IHttpClientFactory that your system under test (SUT) receives that you then use to create an instance of a HttpClient. You may have named clients. Or maybe they are typed clients. You can certainly address each of these scenarios individually but if you are looking for a simpler solution, I have another suggestion for you. There is an extremely popular OSS project out on GitHub (available to your .NET projects as a NuGet package) called Moq.Contrib.HttpClient that helps in this regard. Let’s take a look at it.

System Under Test

Let’s take a look at some sample code that uses IHttpClientFactory and HttpClient.

using MediatR;
using System.Text.Json;

namespace HttpUnitTesting.Weather;

public class WeatherForecastRequest :
    IRequest<WeatherForecastResponse>
{
    // your request params will go here
}

public class GetWeatherForecastHandler : IRequestHandler<WeatherForecastRequest, WeatherForecastResponse>
{
    private readonly HttpClient httpClient;
    public GetWeatherForecastHandler(IHttpClientFactory httpClientFactory)
    {
        httpClient = httpClientFactory.CreateClient();
    }

    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public async Task<WeatherForecastResponse> Handle(WeatherForecastRequest request, CancellationToken cancellationToken)
    {
        var httpResponse = await httpClient.GetAsync("http://www.randomnumberapi.com/api/v1.0/random?min=26&max=38&count=10");

        if (httpResponse.IsSuccessStatusCode)
        {
            var results = await httpResponse.Content.ReadAsStringAsync();
            var forecast = JsonSerializer.Deserialize<int[]>(results)!;
            var weatherForecastResponse = new WeatherForecastResponse
            {
                WeatherForecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = forecast[index],
                    Summary = Summaries[Random.Shared.Next(Summaries.Length)]
                })
            };

            return await Task.FromResult(weatherForecastResponse);
        }

        throw new HttpRequestException("Uh-oh!  Bad stuff happened.");
    }
}

public class WeatherForecastResponse
{
    public IEnumerable<WeatherForecast> WeatherForecasts { get; set; } = new List<WeatherForecast>();
}

Above, I have a MediatR handler that is getting an IHttpClientFactory injected in through its constructor. And within that constructor, a new HttpClient is being created. If you are unfamiliar with MediatR, please see my previous post.

How do I go about unit-testing this handler? Does the Handle method call the external RandomNumber API? Was it an HTTP GET call? If the call succeeded, does the handler generate a 5-day forecast? If the API call failed, does it throw an exception? These are all potential unit tests that I would write for a handler such as this.

Let’s see how we can utilize Moq.Contrib.HttpClient to aid us in that testing.

Setup Moq.Contrib.HttpClient

You can add this library to your project via the dotnet CLI:

dotnet add package Moq.Contrib.HttpClient

Besides this package, I’m using xUnit as my unit-testing library.

Even with this package in place, I noticed that any testing associated with this can get quite verbose. To keep the tests a bit clean, clear and easy to contend with, I decided to write a couple of helper classes to tuck away some of that verbosity.

using Moq;
using Moq.Contrib.HttpClient;
using Moq.Protected;
using System.Net;

namespace HttpUnitTesting.Weather;
public class HttpTestingUtilityParameters
{
    public string HttpClientName { get; set; } = null;
    public HttpResponseMessage ReturnMessage { get; set; } = new HttpResponseMessage()
    {
        StatusCode = HttpStatusCode.OK
    };
}

public class HttpTestingUtility
{
    public HttpTestingUtility(HttpTestingUtilityParameters parameters)
    {
        Handler = new Mock<HttpMessageHandler>();
        Handler.Protected()
            .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(parameters.ReturnMessage)
            .Verifiable();
        Handler.As<IDisposable>().Setup(s => s.Dispose());

        Factory = Handler.CreateClientFactory();

        if (parameters.HttpClientName is not null)
        {
            Mock.Get(Factory).Setup(x => x.CreateClient(parameters.HttpClientName))
            .Returns(() =>
            {
                var client = Handler.CreateClient();
                client.BaseAddress = new Uri("http://www.doesntexist.com/");
                return client;
            });
        }
    }

    public Mock<HttpMessageHandler> Handler { get; }
    public IHttpClientFactory Factory { get; }
}

So what are these helper classes doing? They help in setting up your mock HTTP clients. Are you using a named client? Well you can tell the helper to create you one with a name that you provide. Do you need to mock a particular response when this mock client is utilized? Pass that custom HTTP response to the helper. Do you want something other than a 200 OK when your mock client is invoked? You can set that up too.

Unit Testing

So, we have our libraries and helpers all in place. Let’s write a few unit test for our GetWeatherForecastHandler.cs class.

using Moq.Contrib.HttpClient;
using Xunit;

namespace HttpUnitTesting.Weather;
public class WeatherHandlerTests
{
    [Fact]
    public async Task Handle_CallsRandomNumberExternalApi()
    {
        // Arrange
        var httpTestingUtility = new HttpTestingUtility(new HttpTestingUtilityParameters
        {
            ReturnMessage = new HttpResponseMessage
            {
                Content = new StringContent("[100,99,98,97,96,95,94,93,92,91]")
            }
        });
        var sut = new GetWeatherForecastHandler(httpTestingUtility.Factory);

        // Act
        await sut.Handle(new WeatherForecastRequest(), new CancellationToken());

        // Assert
        // Did the Handle method call the Random Number API?
        httpTestingUtility.Handler.VerifyRequest(x => x.RequestUri.AbsoluteUri.Contains("randomnumberapi.com"));

        // Was it a GET request?
        httpTestingUtility.Handler.VerifyRequest(x => x.Method == HttpMethod.Get);
    }

    [Fact]
    public async Task Handle_Returns5DayForecast()
    {
        // Arrange
        var httpTestingUtility = new HttpTestingUtility(new HttpTestingUtilityParameters
        {
            ReturnMessage = new HttpResponseMessage
            {
                Content = new StringContent("[100,99,98,97,96,95,94,93,92,91]")
            }
        });
        var sut = new GetWeatherForecastHandler(httpTestingUtility.Factory);

        // Act
        var results = await sut.Handle(new WeatherForecastRequest(), new CancellationToken());

        // Assert
        Assert.Equal(5, results.WeatherForecasts.Count());
    }

    [Fact]
    public async Task Handle_ThrowsErrorWhenRandomNumberApiCallFails()
    {
        // Arrange
        var httpTestingUtility = new HttpTestingUtility(new HttpTestingUtilityParameters
        {
            ReturnMessage = new HttpResponseMessage
            {
                StatusCode = System.Net.HttpStatusCode.ServiceUnavailable
            }
        });
        var sut = new GetWeatherForecastHandler(httpTestingUtility.Factory);

        // Act & Assert
        await Assert.ThrowsAsync<HttpRequestException>(async () =>
        {
            await sut.Handle(new WeatherForecastRequest(), new CancellationToken());
        });
    }
}

Closing Remarks

HttpClient and IHttpClientFactory can pose challenges for unit-testing. Moq.Contrib.HttpClient provides some niceties that can ease that pain a bit. Give it a go and let me know what you think.

You can find the code that I referenced in this post, on my GitHub account, here:

https://github.com/tvaidyan/httpclient-testing-demo

Leave a Comment

Your email address will not be published. Required fields are marked *