C# - How to mock a HttpClient dependency for tests - updated 2023

In this post I will go through how you can mock the HttpClient class in C#. This is often needed as almost everything we develop these days communicates with something else. Often this is done through HTTP and for this in C# you will likely be using the HttpClient. However sometimes you wish to test your class in isolation and for this you need to stub the HttpClient.

The HttpMessageHandler

So you have something along the lines of this in your code:

var httpClient = new HttpClient();
return await httpClient.GetAsync("https://peterdaugaardrasmussen.com/SomeFakeUrl"); //Not a real url..

Often you would assume that there is an interface which you can mock. However there is no interface for HttpClient, instead the ability to override its functionality is within the abstract class HttpMessageHandler. This class can be injected into the HttpClient which let you override any request. Since the MessageHandler is abstract you will have to create your own implementation, which could look like the below:

public class HttpMessageHandlerStub : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("This is a reply")
        };

        return await Task.FromResult(responseMessage);
    }
}

In the above we create our own HttpMessageHandler implementation named HttpMessageHandlerStub. This always return the same response. Our new stub is easily invoked by injecting it into the HttpClient and calling a method on it:

public static async Task<HttpResponseMessage> CallHttp()
{
    var httpClient = new HttpClient(new HttpMessageHandlerStub()); //Important part
    return await httpClient.GetAsync("https://peterdaugaardrasmussen.com/SomeFakeUrl");
}

Now when any method is called on the httpClient (like GetAsync) in the above, it will return a 200 response with the content "This is a reply". This is the basis on how to mock, fake and stub the HttpClient.

A more generic approach

In the previous example you would have to create a new stub every time you would want a different response. However using a function you can easily create a reusable implementation:

public class HttpMessageHandlerStub : HttpMessageHandler
{
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsync;

    public HttpMessageHandlerStub(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsync)
    {
        _sendAsync = sendAsync;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _sendAsync(request, cancellationToken);
    }
}

In the above my HttpMessageHandlerStub class now has a constructor that takes a function. This function will be invoked when SendAsync() is called. Which means I can now create stubs with different results like in the below:

var httpClient = new HttpClient(new HttpMessageHandlerStub(async (request, cancellationToken) =>
{
    var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent("This is a reply")
    };

    return await Task.FromResult(responseMessage);
}));

return await httpClient.GetAsync("https://peterdaugaardrasmussen.com/SomeFakeUrl");

This solution gives the exact same result as the first one. However you do not have to create a new version of the HttpMessageHandler interface every time.

Wrapping it up

In the above I have not taken the approach of wrapping HttpClient in another class with an interface. This way you would be able to mock all the methods. This is another approach that I have seen often used. You can likely draw inspiration on my post on how to mock the datetime struct for unit tests.

Remember that the HttpClient is an integration point. It would make sense to test it together with whatever you are calling. That of course does not exclude unit tests, which can still be added. But these will not catch the errors in your communication, for that you will have to do a higher level of testing.

That's it. I hope you enjoyed this post, if or if not please leave a comment below!