2 Powerful Ways to Unit Test HttpClient in C#


We live in a connected world.

Everything is connected to the internet, from our computers to mobile phones, watches, and even washing machines. So naturally, the applications that you build also talk to some API. And when it comes to C#, they most likely use the HttpClient class.

But how do you make sure the application works correctly? How to unit test code that uses HttpClient in C#?

There are two ways to unit test HttpClient in C#:

  • Mock HttpMessageHandler using Moq or some other mocking framework
  • Create a thin wrapper interface around the HttpClient and use that instead of HtttpClient.

This article will show you code examples for both approaches. And you will get all the information to decide which one is the ideal option for you.

How to unit test code that uses HttpClient

HttpClient is a powerful tool for making HTTP requests in .NET.

You can use it to fetch data from a web API or make other types of HTTP requests.

However, HttpClient can be tricky to unit test. That’s because HttpClient makes HTTP requests using the underlying operating system. This means that unit tests that use HttpClient will also make actual HTTP requests. That can lead to slow unit tests and flaky tests that fail intermittently.

Let’s take a look at one code example that uses HttpClient to fetch data from the API:

public class CryptoService
{
    private const string PRICES_ENDPOINT = "simple/price?ids=bitcoin%2Cbitcoin-cash%2Cdash" +
        "%2Cethereum%2Ceos%2Clitecoin%2Cmonero%2Cripple%2Cstellar&vs_currencies=usd";
    private HttpClient _httpClient;

    public CryptoService()
    {
        _httpClient = new HttpClient();
    }

    public async Task<List<Coin>> GetLatestPrices()
    {
        var url = "https://api.coingecko.com/api/v3/" + PRICES_ENDPOINT;

        HttpResponseMessage response = await _httpClient.GetAsync(url);

        string serialized = await response.Content.ReadAsStringAsync();
        var result = JsonConvert
            .DeserializeObject<Dictionary<string, Dictionary<string, double?>>>(serialized);

        var coins = Coin.GetAvailableAssets();
        foreach (var coin in coins)
        {
            var coinPrices = result[coin.Name.Replace(' ', '-').ToLower()];
            var coinPrice = coinPrices["usd"];
            coin.Price = coinPrice ?? 0;
        }

        return coins;
    }
}

What’s the problem while writing unit tests for the above code that uses HttpClient?

Well, it can be tricky to unit test code that uses HttpClient, as HttpClient is not designed to be mocked. HttpClient class doesn’t implement any interface you could use to create a fake substitute in your tests.

Also, keep in mind that the above code is not entirely correct. The HttpClient is instantiated every time a new instance of the CryptoService is created, but according to the Microsoft documentation:

HttpClient is intended to be instantiated once and reused throughout the life of an application. In .NET Core and .NET 5+, HttpClient pools connections inside the handler instance and reuses a connection across multiple requests. If you instantiate an HttpClient class for every request, the number of sockets available under heavy loads will be exhausted. This exhaustion will result in SocketException errors.

That means you need to ensure HttpClient is a singleton in your application.

One approach to test the above code is to leave the code in the current state and write an integration test for the code. That could work, but you need a test server to return test data. And make sure to set up your test server correctly.

Another approach is to change the code to make it suitable for testing.

Let’s see what the options are.

#1 Mock HttpMessageHandler and pass it to the HttpClient

HttpClient has three constructors:

  1. HttpClient()
  2. HttpClient (System.Net.Http.HttpMessageHandler handler);
  3. HttpClient (System.Net.Http.HttpMessageHandler handler, bool disposeHandler);

The first way to unit test HttpClient is to use the second constructor and pass an instance of the HttpMessageHandler to your class. For the above example, the CryptoService would end up looking like this:

public class CryptoService
{
    private const string PRICES_ENDPOINT = "simple/price?ids=bitcoin%2Cbitcoin-cash%2Cdash" +
        "%2Cethereum%2Ceos%2Clitecoin%2Cmonero%2Cripple%2Cstellar&vs_currencies=usd";
    private HttpClient _httpClient;

    public CryptoService(HttpMessageHandler handler)
    {
        _httpClient = new HttpClient(handler);
    }

    public async Task<List<Coin>> GetLatestPrices()
    {
        var url = "https://api.coingecko.com/api/v3/" + PRICES_ENDPOINT;

        HttpResponseMessage response = await _httpClient.GetAsync(url);

        string serialized = await response.Content.ReadAsStringAsync();
        var result = JsonConvert
            .DeserializeObject<Dictionary<string, Dictionary<string, double?>>>(serialized);

        var coins = Coin.GetAvailableAssets();
        foreach (var coin in coins)
        {
            var coinPrices = result[coin.Name.Replace(' ', '-').ToLower()];
            var coinPrice = coinPrices["usd"];
            coin.Price = coinPrice ?? 0;
        }

        return coins;
    }
}

What is HttpMessageHandler?

HttpMessageHandler is an abstract class for message handlers. Message handlers are components that can process HTTP requests and responses.

HttpClient uses the HttpMessageHandler class to send HTTP requests and receive HTTP responses. HttpMessageHandler contains a single SendAsync method that derived classes must implement.

This method is called when the HttpClient wants to send an HTTP request.

To make a fake instance of the HttpMessageHandler, you can use Moq, a popular mocking framework for .NET.

In case you want to learn more about Moq, check out the separate comprehensive article about it. It contains all the basic (and some advanced) examples you might need while writing tests.

Mock using Moq

To provide a mock instance of the HttpMessageHandler, you need to use the Moq.Protected namespace to implement SendAsync in your tests. The reason is that the SendAsync method is protected.

[Fact]
public async Task Test_CryptoService_with_HttpMessageHandler()
{
    var messageHandler = new Mock<HttpMessageHandler>();
    messageHandler.Protected()
        .Setup<Task<HttpResponseMessage>>("SendAsync",
                                      ItExpr.IsAny<HttpRequestMessage>(),
                                      ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(new HttpResponseMessage
        {
            StatusCode = HttpStatusCode.OK,
            Content = new StringContent(API_TEST_CONTENT)
        });
    var cryptoService = new CryptoService(messageHandler.Object);

    var result = await cryptoService.GetLatestPrices();

    result.Should().HaveCount(9);
}

The above test shows how to set up the fake instance of the HttpMessageHandler and mock the SendAsync method.

I’m using the Fluent Assertions Should extension method in the assert phase to have a more readable test. If you want more readable tests, start using the Fluent Assertions library. This article shows you what advantages you will get.

Mock using MockHttp

MockHttp is an open-source library that makes unit testing easier. It is an abstraction layer for HttpClient class. You can use it via fluent syntax in your tests to set up custom HTTP responses.

MockHttp provides an alternative way to set up the HttpMessageHandler.

You can install it via NuGet:

Install-Package RichardSzalay.MockHttp

and use it in your tests.

[Fact]
public async Task Test_CryptoService_with_MockHttp()
{
    var mockHttpMessageHandler = new MockHttpMessageHandler();

    //setup a respond for the simple/price endpoing
    mockHttpMessageHandler
        .When("https://api.coingecko.com/api/v3/simple/price")
        .Respond("application/json", API_TEST_CONTENT); // return JSON

    //inject the handler
    var cryptoService = new CryptoService(mockHttpMessageHandler);

    var result = await cryptoService.GetLatestPrices();

    result.Should().HaveCount(9);
}

MockHttpMessageHandler inherits the HttpMessageHandler and gives you additional methods to set up the handler you will use in your code.

You can also use the wildcard character (*) while calling the When method to have more stable tests.

    //setup a respond for the simple/price endpoing
    mockHttpMessageHandler
        .When("https://api.coingecko.com/api/v3/simple/*")
        .Respond("application/json", API_TEST_CONTENT); // return JSON

MockHttp library also offers the following matchers methods:

  • WithQueryString
  • WithExactQueryString
  • WithFormData
  • WithExactFormData
  • WithContent
  • WithPartialContent
  • WithHeaders
  • With

#2 HttpClient wrapper interface

The previous approach focused on mocking the HttpClient, or its parts to write unit tests. However, there is another approach. You can create a tiny wrapper interface around the HttpClient and use it in your code instead of directly calling the HttpClient. This acts similar to how the Decorator pattern works.

This approach has some benefits, which you will see shortly.

To begin, create a new interface that will wrap the HttpClient’s methods you use in your code.

public interface IHttpClientWrapper
{
    Task<HttpResponseMessage> DeleteAsync(Uri requestUri);
    Task<HttpResponseMessage> GetAsync(string requestUri);
    Task<HttpResponseMessage> PostAsync(string requestUri,
                                        HttpContent content);
    Task<HttpResponseMessage> PutAsync(string requestUri,
                                       HttpContent content);
}

The implement it:

internal class HttpClientWrapper: IHttpClientWrapper
{
    private HttpClient _httpClient;

    public HttpClientWrapper(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public Task<HttpResponseMessage> DeleteAsync(Uri requestUri)
    {
        return _httpClient.DeleteAsync(requestUri);
    }

    public Task<HttpResponseMessage> GetAsync(string requestUri)
    {
        return _httpClient.GetAsync(requestUri);
    }

    public Task<HttpResponseMessage> PostAsync(string requestUri,
                                               HttpContent content)
    {
        return _httpClient.PostAsync(requestUri, content);
    }

    public Task<HttpResponseMessage> PutAsync(string requestUri,
                                              HttpContent content)
    {
        return _httpClient.PutAsync(requestUri, content);
    }
}

Finally, inject and use the interface in the code.

public class CryptoService
{
    private const string PRICES_ENDPOINT = "simple/price?ids=bitcoin%2Cbitcoin-cash%2Cdash" +
        "%2Cethereum%2Ceos%2Clitecoin%2Cmonero%2Cripple%2Cstellar&vs_currencies=usd";
    private IHttpClientWrapper _httpClientWrapper;

    public CryptoService(IHttpClientWrapper httpClientWrapper)
    {
        _httpClientWrapper = httpClientWrapper;
    }

    public async Task<List<Coin>> GetLatestPrices()
    {
        var url = "https://api.coingecko.com/api/v3/" + PRICES_ENDPOINT;

        HttpResponseMessage response = await _httpClientWrapper.GetAsync(url);

        string serialized = await response.Content.ReadAsStringAsync();
        var result = JsonConvert
            .DeserializeObject<Dictionary<string, Dictionary<string, double?>>>(serialized);

        var coins = Coin.GetAvailableAssets();
        foreach (var coin in coins)
        {
            var coinPrices = result[coin.Name.Replace(' ', '-').ToLower()];
            var coinPrice = coinPrices["usd"];
            coin.Price = coinPrice ?? 0;
        }

        return coins;
    }
}

Now you can easily write the tests.

[Fact]
public async Task Test_CryptoService_with_simple_HttpClientWrapper()
{
    var clientWrapper = new Mock<IHttpClientWrapper>();
    clientWrapper
        .Setup(x => x.GetAsync(It.IsAny<string>()))
        .ReturnsAsync(new HttpResponseMessage()
        {
            Content = new StringContent(API_TEST_CONTENT)
        });

    var cryptoService = new CryptoService(clientWrapper.Object);

    var result = await cryptoService.GetLatestPrices();

    result.Should().HaveCount(9);
}

The most significant benefit of having the custom interface is that you can extend it and add additional logic to it:

  • parsing the result to the expected type
  • logging
  • error handling
  • retry policy

To see how you can upgrade the wrapper, let’s add the logic for parsing the result to the expected type.

public interface INetworkService
{
    Task<TResult> GetAsync<TResult>(string uri);
    Task<TResult> PostAsync<TResult>(string uri,
                                     string jsonData);
    Task<TResult> PutAsync<TResult>(string uri,
                                    string jsonData);
    Task DeleteAsync(string uri);
}

public class NetworkService : INetworkService
{
    private HttpClient _httpClient;

    public NetworkService()
    {
        _httpClient = new HttpClient();
    }

    public async Task<TResult> GetAsync<TResult>(string uri)
    {
        var response = await _httpClient.GetAsync(uri);

        string serialized = await response.Content.ReadAsStringAsync();

        return JsonConvert.DeserializeObject<TResult>(serialized);
    }

    public async Task<TResult> PostAsync<TResult>(string uri,
                                                  string jsonData)
    {
        var content = new StringContent(jsonData,
                                        Encoding.UTF8,
                                        "application/json");
        var response = await _httpClient.PostAsync(uri, content);

        string serialized = await response.Content.ReadAsStringAsync();

        return JsonConvert.DeserializeObject<TResult>(serialized);
    }

    public async Task<TResult> PutAsync<TResult>(string uri,
                                                 string jsonData)
    {
        var content = new StringContent(jsonData,
                                        Encoding.UTF8,
                                        "application/json");
        var response = await _httpClient.PutAsync(uri, content);

        string serialized = await response.Content.ReadAsStringAsync();

        return JsonConvert.DeserializeObject<TResult>(serialized);
    }

    public async Task DeleteAsync(string uri)
    {
        await _httpClient.DeleteAsync(uri);
    }
}

First, the NetworkService class calls the methods of the HttpClient. Then it goes one step further and uses JsonConvert to parse the returned string to the expected type. That way, you can eliminate the parsing logic from the CryptoService (and all other classes that call the HttpClient directly).

public class CryptoService3
{
    private const string PRICES_ENDPOINT = "simple/price?" +
        "ids=bitcoin%2Cbitcoin-cash%2Cdash" +
        "%2Cethereum%2Ceos%2Clitecoin%2Cmonero%2Cripple%2C" +
        "stellar&vs_currencies=usd";

    private readonly INetworkService _networkService;

    public CryptoService3(INetworkService networkService)
    {
        _networkService = networkService;
    }

    public async Task<List<Coin>> GetLatestPrices()
    {
        var url = "https://api.coingecko.com/api/v3/" + PRICES_ENDPOINT;

        var result = await _networkService
            .GetAsync<Dictionary<string, Dictionary<string, double?>>>(url);

        var coins = Coin.GetAvailableAssets();
        foreach (var coin in coins)
        {
            var coinPrices = result[coin.Name.Replace(' ', '-').ToLower()];
            var coinPrice = coinPrices["usd"];
            coin.Price = coinPrice ?? 0;
        }

        return coins;
    }
}

Finally, the test.

[Fact]
public async Task Test_CryptoService_with_NetworkService()
{
    var networkServiceMock = new Mock<INetworkService>();
    networkServiceMock
        .Setup(x => x.GetAsync<Dictionary<string, Dictionary<string, double?>>>(It.IsAny<string>()))
        .ReturnsAsync(new Dictionary<string, Dictionary<string, double?>>
        {
            { "bitcoin", new Dictionary<string, double?> { { "usd", 20156 } } },
            { "bitcoin-cash", new Dictionary<string, double?> { { "usd", 115.09 } } },
            { "eos", new Dictionary<string, double?> { { "usd", 0.935071 } } },
            { "stellar", new Dictionary<string, double?> { { "usd", 0.113627 } } },
            { "monero", new Dictionary<string, double?> { { "usd", 114.76 } } },
            { "litecoin", new Dictionary<string, double?> { { "usd", 51.93 } } },
            { "dash", new Dictionary<string, double?> { { "usd", 46.0 } } },
            { "ripple", new Dictionary<string, double?> { { "usd", 0.322946 } } },
            { "ethereum", new Dictionary<string, double?> { { "usd", 1077.82 } } },
        });

    var cryptoService = new CryptoService3(networkServiceMock.Object);

    var result = await cryptoService.GetLatestPrices();

    result.Should().HaveCount(9);
}

The test now works with custom types instead of strings.

FAQ

faq

What is the difference between HttpClient and HttpWebRequest?

The HttpWebRequest class is used to send HTTP requests to a server. It allows you to specify the URL of the request, the HTTP method (GET, POST, etc.), the type of data to be sent in the request body, and various other parameters.

The main difference between HttpClient and HttpWebRequest is that HttpClient is easier to use because it has a simpler API. On the other hand, HttpWebRequest requires more code to use. Also, Microsoft recommends using HttpClient for any new code you produce.

How do I use HttpRequestMessage with HttpClient?

The HttpRequestMessage represents an HTTP message. You can use this class with the HttpClient class to send HTTP requests and receive HTTP responses from a web server.

To use the HttpRequestMessage class, you first need to create an instance of the class. You can do this by using the HttpRequestMessage constructor. Once you have an instance of the class, you can set the various properties of the HTTP request, such as the request method, request URI, and request headers.

Once you have set all the properties of the HTTP request, you can then use the SendAsync method of the HttpClient class to send the HttpRequestMessage in the request and receive the response from the web server.

Is HttpClient a singleton?

HttpClient is not a singleton in its implementation. However, Microsoft recommends that you instantiate it once and use the same instance throughout the life of your application. HttpClient pools and reuses connections across multiple requests.

By having more instances of the HttpClient, you can exhaust the number of available sockets if there is a heavy load. Exhaustion can lead to exceptions.

Conclusion

Unit testing code that contains HttpClient is possible and advisable. Some challenges come with unit testing code containing HttpClient, but these challenges can be overcome with careful planning and thought.

Ultimately, unit testing code containing HttpClient can help ensure that your code is working as intended and can help catch any potential errors.

Recent Posts