Mocking HttpClient SendAsync

In the past the only way I’ve known how to deal with mocking Http/Web clients is to wrap the methods using a custom interface. This is a lot of overhead and I feel teaches us its ok to write code that was not written in a way that is testable from the start. Wrappers are a smell in my opion.

.Net Core have solved the HttpClient problem by providing a way to inject a HttpClient instance using .AddHttpClient which is in the Microsoft.Extensions.DependencyInjection namespace. This is great as it then gives the consumer a valid object for them to setup things like their BaseAddress and other settings needed.

Mock HttpMessageHandler

  1. In the pipeline add
1
2
services
.AddHttpClient<IFooServiceClient, FooServiceClient>()
  1. In the constructor for FooServiceClient you will automagically get a HttpClient instance which you can then set any propertys needed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class FooServiceClient 
{
private readonly HttpClient _client;

public FooServiceClient(HttpClient client, IConfiguration config)
{
client.BaseAddress = new Uri(config.GetValue<string>("Services:FooService:Url"));

_client = client;
}

public async Task<SomeClass> SomeMethod()
{
var requestUri = "http://localhost:81";
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
//add headers to request if required

var response = await _client.SendAsync(request);
response.EnsureSuccessStatusCode();

// TODO: do something with the response :D
return new SomeClass();
}
}
  1. Now if you try mock to perhaps assert what the HttpRequestMessage request was, or even that SendAsync was called it cannot be done as it has no override modifier. If you try construct the mock it will protest at runtime about setup / verification expressions.
1
2
3
var httpClientMock = new Mock<HttpClient>();
httpClientMock
.Verify(x => x.SendAsync(It.IsAny<HttpRequestMessage>()), Times.Once);

Injecting the above with httpClientMock.Object will be fine but the .Verify will fail as expected.

1
2
3
Message: 
System.NotSupportedException : Unsupported expression: x => x.SendAsync(It.IsAny<HttpRequestMessage>())
Non-overridable members (here: HttpClient.SendAsync) may not be used in setup / verification expressions.

3.1. The work around is to look at the HttpClient constructor overloads, one takes an instance of HttpMessageHandler which has an abstract SendAsync method that is used by HttpClient under the hood. Credit to Gaute Meek Olsen’s post explaining this far better than I could.

Create the httpMessageHandlerMock

1
2
var httpMessageHandlerMock = new Mock<HttpMessageHandler>();
var response = new HttpResponseMessage { StatusCode = HttpStatusCode.OK };

You can also add to the response body if needs be, the below is manual json - best to do it with a serialized object of what ever your end point returns.

1
2
3
4
var response = new HttpResponseMessage { 
StatusCode = HttpStatusCode.OK
Content = new StringContent(@"[{ ""id"": 1, ""desc"": ""foo""}]")
};

Then define a .Setup to return the response above. As SendAsync is protected we need to use .Protected()

Note that if in your implementation of HttpClient calls .GetAsync(...) this will still work as internally GetAsync will eventually call SendAsync.

1
2
3
4
5
6
7
httpMessageHandlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);

Create the client for dependancy injection

1
2
var httpClient = new HttpClient(httpMessageHandlerMock.Object);
// Then pass `httpClient` to `FooServiceClient` and call `SomeMethod`

A mock .Verify to check SendAsync was called once with an expected request.

1
2
3
4
5
6
7
8
9
10
httpMessageHandlerMock
.Protected()
.Verify(
"SendAsync",
Times.Exactly(1),
ItExpr.Is<HttpRequestMessage>(
request =>
request.Method == HttpMethod.Get
&& // other checks like `request.RequestUri`, `request.Headers.GetValues` ect),
ItExpr.IsAny<CancellationToken>());

Beauti!

References

Delegating Handler

Alternatively we can use a named http client and add a custom handler using the base class DelegatingHandler

Create the handler

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StubDelegatingHandler : DelegatingHandler
{
private readonly HttpStatusCode _stubHttpStatusCode;
public StubDelegatingHandler(HttpStatusCode stubHttpStatusCode)
{
_stubHttpStatusCode = stubHttpStatusCode;
}

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(new HttpResponseMessage(stubHttpStatusCode));
}
}

Inject in tests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private const string NamedHttpClient = "MyClient";

var expectedHttpStatusCode = HttpStatusCode.OK;
var services = new ServiceCollection();

services
.AddHttpClient(NamedHttpClient)
.AddHttpMessageHandler(() => new StubDelegatingHandler(expectedHttpStatusCode));

var configuredClient =
services
.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>()
.CreateClient(NamedHttpClient);

var result = await configuredClient.GetAsync("https://carlpaton.github.io/");
result.StatusCode.Should().Be(expectedHttpStatusCode);

Boom!

References