Easy way to mock HTTP responses.

Serhii Korol - Oct 1 '23 - - Dev Community

Hello folks. In this article, I want to teach you how to mock HTTP responses easily and apply them to your unit tests.

Let's create a console application:

dotnet new console -n TestHttpClientSample
Enter fullscreen mode Exit fullscreen mode

The Program.cs class you can delete, it no need. We'll begin by adding NuGet packages, and you should add all of these:

<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.11" />
      <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0-preview-23424-02" />
      <PackageReference Include="xunit" Version="2.5.2-pre.2" />
      <PackageReference Include="xunit.runner.visualstudio" Version="2.5.2-pre.3">
Enter fullscreen mode Exit fullscreen mode

And now, for convenience, let's create a test server. Sure, it would be more correct to use WebApplicationFactory, but I'll be demonstrating testing API. I ask you to create a new class and add this code:

public class HttpClientMock : IAsyncDisposable
{
    private bool _running;

    public HttpClientMock()
    {
        var builder = WebApplication.CreateBuilder();
        builder.WebHost.UseTestServer();
        Application = builder.Build();
    }

    public WebApplication Application { get; }

    public HttpClient CreateHttpClient()
    {
        StartServer();
        return Application.GetTestClient();
    }

    private void StartServer()
    {
        if (_running) return;
        _running = true;
        _ = Application.RunAsync();
    }

    public async ValueTask DisposeAsync() => await Application.DisposeAsync();
}
Enter fullscreen mode Exit fullscreen mode

This class creates, starts, and disposes of HttpClient.
Also, creating a small model for deserializing the HTTP requests would be best.

public record CatFact
{
    [JsonProperty("fact")]
    public string Fact { get; set; }

    [JsonProperty("length")]
    public int Length { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

When everything is ready, we are able to create unit tests. Create any class, and it is not fundamental. And add the first test:

[Fact]
    public async Task CatFact_ReturnsNotNullAndNotEmpty()
    {
        await using var clientMock = new HttpClientMock();
        var expectedFact = new CatFact { Fact = "Cats are funny", Length = 14 };
        clientMock.Application.MapGet("/fact", () => expectedFact ).RequireHost("catfact.ninja");

        using var httpClient = clientMock.CreateHttpClient();
        var response = await httpClient.GetAsync("https://catfact.ninja/fact");
        var fact = await response.Content.ReadFromJsonAsync<CatFact>();

        Assert.NotNull(fact);
        Assert.Equal(expectedFact.Length, fact.Length);
        Assert.Equal(expectedFact.Fact, fact.Fact);
    }
Enter fullscreen mode Exit fullscreen mode

In the first part of the test, we create a mock client and determine what will return. In our case, we know that this API returns a JSON object. In the second part, we make the HTTP request and parse content. It's a super easy way to mock any endpoints. If you have not directly called HttpClient into some service, you can pass this mocked client through the constructor. For instance:

using var httpClient = context.CreateHttpClient();
var service = new SomeService(httpClient);
Enter fullscreen mode Exit fullscreen mode

Let's show you a more complicated test.

[Theory]
[InlineData(14)]
[InlineData(13)]
[InlineData(12)]
public async Task CatFact_ReturnsLimitedByLengthResult(int maxLimit)
    {
        await using var clientMock = new HttpClientMock();
        var expectedFact = new CatFact { Fact = "Cats are funny", Length = 14 };
        clientMock.Application.MapGet("/fact",
                async context => {
                    if (int.TryParse(context.Request.Query["max_length"], out int limit))
                    {
                        if (expectedFact.Length == limit)
                        {
                            var json = JsonConvert.SerializeObject(expectedFact);
                            context.Response.ContentType = "application/json";
                            context.Response.StatusCode = StatusCodes.Status200OK;
                            await context.Response.WriteAsync(json);
                        }
                        else
                        {
                            var json = JsonConvert.SerializeObject(default(CatFact));
                            context.Response.ContentType = "application/json";
                            context.Response.StatusCode = StatusCodes.Status200OK;
                            await context.Response.WriteAsync(json);
                        }
                    }
                    else
                    {
                        var json = JsonConvert.SerializeObject(expectedFact);
                        context.Response.ContentType = "application/json";
                        context.Response.StatusCode = StatusCodes.Status200OK;
                        await context.Response.WriteAsync(json);
                    }
                })
            .RequireHost("catfact.ninja");

        using var httpClient = clientMock.CreateHttpClient();
        HttpResponseMessage response = null;
        if (maxLimit == 12)
        {
            var notValidQueryParam = "string";
            response = await httpClient.GetAsync($"https://catfact.ninja/fact?max_length={notValidQueryParam}");
        }
        else
        {
            response = await httpClient.GetAsync($"https://catfact.ninja/fact?max_length={maxLimit}");
        }
        var fact = await response.Content.ReadFromJsonAsync<CatFact>();
        if (maxLimit >= expectedFact.Length)
        {
            Assert.NotEqual(default, fact);
            Assert.Equal(expectedFact.Length, fact.Length);
            Assert.Equal(expectedFact.Fact, fact.Fact);
        }
        else if (maxLimit < expectedFact.Length && maxLimit != 12)
        {
            Assert.Equal(default, fact);
        }
        else if (maxLimit == 12)
        {
            Assert.NotEqual(default, fact);
            Assert.Equal(expectedFact.Length, fact.Length);
            Assert.Equal(expectedFact.Fact, fact.Fact);
        }
    }
Enter fullscreen mode Exit fullscreen mode

As you can see, we handle query parameters and return appropriate responses. Separately, I want to say that WebApplication allows disabling RateLimit, enabling CORS policy, or enabling authorization. You do not need to use stab when you can just mock the client and return the required result for your case.

I hope this article was helpful.

That's all. Have a nice day, and happy coding.

Source code: HERE

Buy Me A Beer

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .