Faking HttpClient
Let's assume that you want to create a fake HttpClient
so you can dictate the
behavior of the
GetAsync(String)
method. 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
:
This method is neither virtual nor abstract, and so can't be overridden by FakeItEasy.
As a workaround, we can look at the definition of
GetAsync
and see that the method eventually ends up calling
HttpMessageHandler.SendAsync(HttpRequestMessage,
CancellationToken)
on an HttpMessageHandler
that can be supplied via the HttpClient
constructor.
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.
With this knowledge, we can write a passing test:
This is a simplified example
In the interest of brevity, we create a Fake, exercise it directly, and check its behavior. A more realistic example would create the Fake as a collaborator of some production class (the "system under test") and the Fake would not be called directly from the test code.
[Fact]
public async Task FakeAnyMethodWay()
{
using 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);
using 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.
public abstract class FakeableHttpMessageHandler : HttpMessageHandler
{
public abstract Task<HttpResponseMessage> FakeSendAsync(
HttpRequestMessage request, CancellationToken cancellationToken);
// sealed so FakeItEasy won't intercept calls to this method
protected sealed override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
=> this.FakeSendAsync(request, cancellationToken);
}
[Fact]
public async Task FakeByMakingMessageHandlerFakeable()
{
using 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);
using 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");
}
Faking PostAsync
The techniques above can be used to intercept calls to methods other than GetAsync
as well.
Consider this test, which ensures that the correct content is passed to HttpClient.PostAsync
.
Does not work for .NET Framework
When run under .NET Framework, the request content is disposed as soon as the request is made, as explained in HttpClient source code comments.
[Fact]
public async Task FakePostAsync()
{
var handler = A.Fake<FakeableHttpMessageHandler>();
using var client = new HttpClient(handler);
using var postContent = new StringContent("my post");
await client.PostAsync(
"https://fakeiteasy.github.io/docs/", postContent);
A.CallTo(() => handler.FakeSendAsync(
A<HttpRequestMessage>.That.Matches(
m => m.Content!.ReadAsStringAsync().Result == "my post"),
A<CancellationToken>.Ignored))
.MustHaveHappened();
}