Note: phiên bản Tiếng Việt của bài này ở link dưới.

https://duongnt.com/mock-ihttpclientfactory-vie

Mock IHttpClientFactory for unit testing

The IHttpClientFactory interface was introduced in .NET Core 2.1. It makes creating and configuring HttpClient instances much simpler.

However, mocking an IHttpClientFactory for unit testing is not straightforward. In today’s article, we will explore two solutions to this problem.

  • Use FakeItEasy to create a mock.
  • Manually implement a mock.

You can download the sample code from the link below.

https://github.com/duongntbk/MockIHttpClientFactoryDemo

Our sample project

In a previous article, I introduced a Python package called passpwnedcheck to check for leaked passwords. Today we will replicate part of that package in C#.

A class to query haveibeenpwned API

We use this PassPwnedCheckClient class to send the hash prefix to haveibeenpwned API and parse the response into a dictionary. Its constructor receives an IHttpClientFactory and uses that factory to create a HttpClient instance.

public PassPwnedCheckClient(IHttpClientFactory httpClientFactory) =>
    _httpClient = httpClientFactory.CreateClient(Constants.HttpClientName);

Then we use the HttpClient instance to send a query to haveibeenpwned API.

var request = new HttpRequestMessage
{
    Method = HttpMethod.Get,
    RequestUri = fullUri,
};

var response = await _httpClient.SendAsync(request);
var rawHashes = await response.Content.ReadAsStringAsync();

return rawHashes.Split("\r\n").Select(r => r.Split(":")).ToDictionary(r => r[0], r => int.Parse(r[1]));

An problem when testing our class

Of course, we don’t want to connect to the real haveibeenpwned API as part of our unit tests. Instead, we want to pass a mock IHttpClientFactory to the constructor of PassPwnedCheckClient. In a naive approach, that mock object will return a mock HttpClient when we call the CreateClient method. Then we can configure the mock HttpClient to return whatever test data we want. This is a simple task, isn’t it?

var httpClient = A.Fake<HttpClient>();
A.CallTo(() => httpClient.GetAsync("<expected request uri>")).Returns(<expected result>);
var httpClientFactory = A.Fake<IHttpClientFactory>
A.CallTo(() => httpClientFactory.CreateClient(Constants.HttpClientName)).Returns(httpClient);

Unfortunately, the approach above does not work because the GetAsync method of the HttpClient class is non-virtual. And common mocking frameworks can only mock virtual methods.

How to mock IHttpClientFactory correctly

How HttpClient makes a request

Let’s take a look at the flow of the GetAsync method in the HttpClient class. All overloads of that method calls this implementation. That method in-turn calls the implementation in the base class.

response = await base.SendAsync(request, cts.Token).ConfigureAwait(false);
ThrowForNullResponse(response);

Next, let’s look at the same method in the base class, which is HttpMessageInvoker.

return await handler.SendAsync(request, cancellationToken).ConfigureAwait(false);

Where handler is an instance of type HttpMessageHandler. This handler is passed to the constructor of both HttpClient and HttpMessageInvoker.

public HttpClient(HttpMessageHandler handler, bool disposeHandler) : base(handler, disposeHandler)
{
    _timeout = s_defaultTimeout;
    _maxResponseContentBufferSize = HttpContent.MaxBufferSize;
    _pendingRequestsCts = new CancellationTokenSource();
}

With this knowledge, we have a way to return test data from HttpClient. All we need to do is to create a mock object of type HttpMessageHandler, pass it to the constructor of HttpClient, and configure it to return whatever we want. Today, we will create that mock object in two ways.

  • Use the FakeItEasy mocking framework.
  • Implement a custom message handler by ourselves.

Create a mock with FakeItEasy

In C# .NET, FakeItEasy is my go-to mocking framework. With it, creating a mock object of type HttpMessageHandler is easy.

var handler = A.Fake<HttpMessageHandler>();

The next step is to mock the return value of the SendAsync method. Fortunately, this is an abstract method, so we can mock it. But because it is protected internal instead of public, the syntax is a bit convoluted.

var response = new HttpResponseMessage
{
    StatusCode = HttpStatusCode.OK,
    Content = new StringContent($"<test content>")
};
A.CallTo(handler).Where(x => x.Method.Name == "SendAsync")
    .WithReturnType<Task<HttpResponseMessage>>()
    .Returns(Task.FromResult(response));

With the setup above, all calls to a method named SendAsync with the return type equal to Task<HttpResponseMessage> will return our test data.

However, as I mentioned in a previous article, we should always match the list of arguments as precisely as possible. We should check if the request Uri is what we expected. This can also be achieved with FakeItEasy.

A.CallTo(handler).Where(x => x.Method.Name == "SendAsync" &&
    (x.Arguments[0] as HttpRequestMessage).RequestUri ==
        new Uri($"<expected request uri>"))
.WithReturnType<Task<HttpResponseMessage>>()
.Returns(Task.FromResult(response));

You can find the complete test case here.

Manually implement a mock handler

As mentioned above, the SendAsync method in the HttpMessageHandler class is abstract. This means we can subclass HttpMessageHandler and provide our own logic for SendAsync. Here is one such custom handler. Below are the important bits.

private readonly Uri _requestUri;
private readonly string _result;

public MockMessageHandler(Uri requestUri, string result)
{
    _requestUri = requestUri;
    _result = result;
}

Provide the test requestUri and the corresponding result.

HttpResponseMessage response;
if (request.RequestUri == _requestUri)
{
    response = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = new StringContent(_result)
    };
}
else
{
    response = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.NotFound
    };
}

return Task.FromResult(response);

If a call was made to _requestUri then return _result. Otherwise, return a 404 - NotFound error.

The last step is to use this custom handler in a unit test. You can find the complete test case here.

var httpClient = new HttpClient(handler);
var httpClientFactory = A.Fake<IHttpClientFactory>();
A.CallTo(() => httpClientFactory.CreateClient(Constants.HttpClientName)).Returns(httpClient);

Conclusion

Although not straightforward, we can still mock an IHttpClientFactory by creating a mock HttpMessageHandler. If the requests made with HttpClient are simple then FakeItEasy can quickly give us a mock. But when we want to have more control, subclassing the HttpMessageHandler class might be a more suitable approach.

A software developer from Vietnam and is currently living in Japan.

One Thought on “Mock IHttpClientFactory for unit testing”

Leave a Reply