How to test HttpClient inside API Tests

02/09/2024

I am a great fan of API tests. I do like to utilize the WebApplicationFactory for this. I even wrote some articles about this in the past "Introduction to WebApplicationFactory". But what if we need to "mock" HttpClient calls inside our tests?

The setup

Imagine the very minimalistic setup:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<GreeterService>(
    o => o.BaseAddress = new Uri("https://steven-giesel.com"));
var app = builder.Build();
app.MapGet("/", async (GreeterService service) => await service.GetSomethingAsync());

app.Run();

public partial class Program;

public sealed class GreeterService
{
    private readonly HttpClient _client;

    public GreeterService(HttpClient client) => _client = client;

    public async Task<string> GetSomethingAsync() => await _client.GetStringAsync("/api");
}

We are utilizing a HttpClient inside our GreeterService. Now this class isn't really mockable. How should we approach that? Because for sure we don't want to make real HTTP requests in our tests (at least to external systems) as they make the test unstable and flaky!

The test

So let's write a very basic test with the WebApplicationFactory:

public class ApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public ApiTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }
    
    [Fact]
    public async Task ShouldGreet()
    {
        using var client = _factory.CreateClient();

        var response = await client.GetStringAsync("/");

        Assert.NotNull(response);
    }
}

Now this test works just fine and will probably pass (well if my website isn't down). But we want to bring everything under our control to make that test:

  1. Stable
  2. Able to return fake data

Meet:

HttpMessageHandler / DelegatingHandler

DelegatingHandler is a "middleware" for your HttpClient. So everytime you call SendAsync that handler will be invoked. Normally you would use it to do some logging or do authentication stuff and what not. But we can also use it in testing scenarios!

The handler for our test will look like this (I will put the link to the code at the end of the blog post):

public class FakeDataHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.RequestUri is { AbsoluteUri: "https://steven-giesel.com/api" })
        {
            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent("Hello World"),
            });
        }
        
        return base.SendAsync(request, cancellationToken);
    }
}

Now we only have to use the Handler:

public ApiTests(WebApplicationFactory<Program> factory)
{
    _factory = factory.WithWebHostBuilder(s =>
        s.ConfigureTestServices(sc =>
        {
            sc.AddTransient<FakeDataHandler>();
            sc.ConfigureHttpClientDefaults(d =>
                d.AddHttpMessageHandler<FakeDataHandler>());
        }));
}

And if we build a test like this, it will pass:

[Fact]
public async Task ShouldGreet()
{
    using var client = _factory.CreateClient();

    var response = await client.GetStringAsync("/");

    Assert.NotNull(response);
    Assert.Equal("Hello World", response);
}

Hurray! We did it. A super convenient way of mocking your Http requests via the HttpClient. And you can easily add extensions here:

  1. Instead of hardcoding the data, you could also get it directly from the DI container. Where you have a mapping between an expected Uri and the response as string or JSON object.
  2. You could also throw an exception, if the route isn't hit. So fail fast! Also bonus points: As you don't make unnecessary requests to a service!

Resources

  • GitHub Repository with all my examples: here
  • Code for this specific blog post: here

How to unit test a RavenDB

RavenDB is a well known open-source document-oriented databse for .NET. And of course we want to test our logic and not only locally while developing, but also our continuous integration pipeline should be able to run our tests. So let's tackle exactly that.

Introduction to WebApplicationFactory

This article will show you what exactly a WebApplicationFactory is and why it is so "mighty" when it comes down to testing. I will highlight some of the properties, which you can leverage to write powerful tests.

Does an HttpClient await the Header and the body?

If you call HttpClient.GetAsync or HttpClient.PostAsync and then await the result, does the await wait only for the headers to be received or does it wait for the body to be received? Let's find out!

An error has occurred. This application may no longer respond until reloaded. Reload x