HttpClient and Unit Testing

If you’ve written C# which uses HttpClient and tried to unit test it, you probably noticed that it’s not the easiest thing in the world to mock out. If this github issue is any indication, many developers don’t find it particularly easy either. It’s certainly not impossible, but it requires learning about some of the internals of HttpClient, such as HttpMessageHandler as the HttpClient is designed as just a wrapper around these things.

Using Moq

Over time I’ve personally solved this problem again and again for different projects by implementing a mock HttpMessageHandler, either explicitly or using a framework like Moq to setup the method calls and return values. The problem is that with these solutions, I found that it was either too loose in terms of ability to validate the code, or too complicated to get the level of strictness I wanted.

Here’s an example from the github issue above for how to mock a request using Moq:

var requestUri = new Uri("http://google.com");
var expectedResponse = "Response text";

var mockResponse = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) };
var mockHandler = new Mock<HttpClientHandler>();
mockHandler
    .Protected()
    .Setup<Task<HttpResponseMessage>>(
        "SendAsync",
        It.Is<HttpRequestMessage>(message => message.RequestUri == requestUri),
        It.IsAny<CancellationToken>())
    .Returns(Task.FromResult(mockResponse));

var httpClient = new HttpClient(mockHandler.Object);
var result = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false);
Assert.AreEqual(expectedResponse, result);

That sets up a mock which matches a call to a specific url and returns a mock response. It works well enough, but surely there’s a better way.

Patterns in other languages/frameworks

While working on some tests for an Angular project, I realized how much nicer their http testing library is and how easy it was to expect specific requests, do additional validations on the details of the request, and respond. Here’s an example:

let logInSuccessful = false;
let error: Error;

let httpMock = TestBed.get(HttpTestingController) as HttpTestingController;
let authenticationService = TestBed.get(AuthenticationService) as AuthenticationService;
authenticationService.logInWithPassword("someUsername", "somePassword")
  .then(() => logInSuccessful = true)
  .catch(e => error = e);

let request = httpMock.expectOne({ method: "post", url: "/api/auth/token" });
expect(request.request.body).toEqual("grant_type=password&username=someUsername&password=somePassword&scope=openid%20offline_access");

let response = createMockResponse();
request.flush(response);
tick();

expect(logInSuccessful).toEqual(true);
expect(error).toBeUndefined();

httpMock.verify();

The Solution

So with that, I decided to build a .NET library to implement a similar pattern as what I was using in the Angular test. It’s on Nuget.org as Testing.HttpClient (Github).

Here’s an example of a unit test using the library:

[TestMethod]
public async Task ExampleTest()
{
    using (var http = new HttpClientTestingFactory())
    {
        var worker = new Worker(http.HttpClient);

        // Make the call, but don't await the task
        var resultTask = worker.FetchDataAsync();

        // Expect the request and respond to it
        var request = http.Expect("http://some-website.com/some-path");
        request.Respond(HttpStatusCode.OK, "123");

        // Await the result and assert on it
        var result = await resultTask;
        Assert.AreEqual(123, result);
    }
}

Design tradeoffs

Because I based this library heavily off Angular’s http testing, the Expect call must be made after the request is made or else it won’t match a request and fail the test. This means that your test needs to separate the calls that start the http call and the one that awaits it. This design decision makes it easy to inspect the actual full request and simplifies the mocking logic.

However, this admittedly may be a little awkward for C# developers who haven’t seen this pattern which is heavily used in Angular testing. I describe this pattern as “synchronously testing asynchronous code”. For those familiar with Angular, this is the difference between using async and fakeAsync.

In a future post I plan on diving into the pros and cons of both testing approaches in more depth.

2 thoughts on “HttpClient and Unit Testing

  1. Getting this error when i try to download”Could not install package ‘Testing.HttpClient 1.0.0’. You are trying to install this package into a project that targets ‘.NETFramework,Version=v4.5’, but the package does not contain any assembly references or content files that are compatible with that framework. For more information, contact the package author.”

    • This library targets netstandard2.0 and based on that error you’re targeting net45, which does not support referencing netstandard2.0 assemblies. You’ll need to upgrade to target a newer framework like 4.6.2 (net462) or 4.7.2 (net472).

Leave a Reply

Your email address will not be published. Required fields are marked *