Serhii Korol
Posted on October 1, 2023
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
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">
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();
}
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; }
}
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);
}
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);
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);
}
}
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
Posted on October 1, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.