Faking HttpClient Using FakeItEasy
Recently in the FakeItEasy gitter channel, someone asked how to fake System.Net.Http.HttpClient.
This is a question that comes up from time to time, and each time I have to fumble for an answer and search old StackOverflow answers (usually for an older version of HttpClient) and the like. Today I'm writing an answer down so it's easier to find.
Let's assume that you want to create a fake HttpClient
so you can dictate the behaviour of the
GetAsync(String)
method. Other methods work similarly. This seems like it would be a straightforward task,
but it's complicated by the design of HttpClient
, which is not faking-friendly.
A working Fake
First off, let's look at the declaration of GetAsync
:
public Task<HttpResponseMessage> GetAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri)
This method is neither virtual nor abstract, and so can't be overridden by FakeItEasy.
This could be the end of the story, but we can look at the
definition of GetAsync
and see that we eventually end up calling
HttpMessageHandler.SendAsync(HttpRequestMessage, CancellationToken)
on an HttpMessageHandler
that can be supplied via the HttpClient
constructor.
The downside is that HttpMessageHandler.SendAsync
is protected, which makes it less convenient to
override than a public method. We need to specify the call by name, and to give FakeItEasy a hint about the return type,
as described in Specifying a call to any method or property.
Now we can write the following passing test:
public async Task Test() { var response = new HttpResponseMessage { Content = new StringContent("FakeItEasy is fun") }; var handler = A.Fake<HttpMessageHandler>(); A.CallTo(handler) .WithReturnType<Task<HttpResponseMessage>>() .Where(call => call.Method.Name == "SendAsync") .Returns(response); var client = new HttpClient(handler); var result = await client.GetAsync("https://fakeiteasy.github.io/docs/"); var content = await result.Content.ReadAsStringAsync(); content.Should().Be("FakeItEasy is fun"); }
Easier and safer call configuration
The above code works, but specifying the method name and return type is a little awkward.
A FakeableHttpMessageHandler
class can be used to clean things up and to also supply a
little compile-time safety by ensuring we're configuring the expected method.
(Note: this class is a near-verbatim copy of the one written by FakeItEasy
co-owner Thomas Levesque while we were answering the user's question.)
public abstract class FakeableHttpMessageHandler : HttpMessageHandler { // sealed so when FakeItEasy creates a Fake, it won't intercept calls protected sealed override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) => FakeSendAsync(request, cancellationToken); public abstract Task<HttpResponseMessage> FakeSendAsync( HttpRequestMessage request, CancellationToken cancellationToken); } public async Task Test() { var response = new HttpResponseMessage { Content = new StringContent("FakeItEasy is fun") }; var handler = A.Fake<FakeableHttpMessageHandler>(); A.CallTo(() => handler.FakeSendAsync(A<HttpRequestMessage>.Ignored, A<CancellationToken>.Ignored)) .Returns(response); var client = new HttpClient(handler); var result = await client.GetAsync("https://fakeiteasy.github.io/docs/"); var content = await result.Content.ReadAsStringAsync(); content.Should().Be("FakeItEasy is fun"); }
Alternative: wrap HttpClient
The above approach will work, but is a little cumbersome, and relies on the internal
implementation of HttpClient
remaining the same. Assuming the interfaces of the
production code can be changed, one way to reduce uncertainty and
future-proof the code is to introduce a layer of abstraction on top of HttpClient
.
Since the wrapper could only be tested by faking HttpClient, which is what got us
into this mess, or by actually making web requests, we keep the implementation as
simple as possible and either lightly test the wrapper or leave it untested.
public interface IWebStringGetter { Task<string> GetAsync(String requestUri); } public class WebStringGetter : IWebStringGetter { private readonly HttpClient client; public WebStringGetter(HttpClient client) => this.client = client; public async Task<string> GetAsync(string requestUri) => await client.GetAsync(requestUri).Result.Content.ReadAsStringAsync(); } public async Task Test() { var getter = A.Fake<IWebStringGetter>(); A.CallTo(() => getter.GetAsync("https://fakeiteasy.github.io/docs/")) .Returns("FakeItEasy is fun"); var text = await getter.GetAsync("https://fakeiteasy.github.io/docs/"); text.Should().Be("FakeItEasy is fun"); }
This results in a much simpler test, and so long as the HttpClient
doesn't change its interface,
it will continue to work. Moreover, this technique is applicable to all kinds of difficult-to-fake
collaborators.