Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/mock-ihttpclientfactory-vie
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.
One Thought on “Mock IHttpClientFactory for unit testing”