Mocking HttpClient requests for C# unit tests
Integrating with an external API is something that I do over and over again. While a lot of APIs have clients built specifically for .NET, spinning up a new HttpClient
is at least a monthly pleasure. Calling an API using HttpClient
is easy, but unit testing code using a HttpClient
is not always a walk in the park. In this post, I'll introduce you to a way of mocking requests when using a HttpClient
.
For the rest of this post, I'll use a simple example. I have implemented a quick CLI that can output jokes using the free JokeAPI. Who doesn't want a CLI dedicated to generating programming jokes? The joke fetching is implemented in a Jokes
class:
public class Jokes(HttpClient httpClient)
{
private readonly HttpClient httpClient = httpClient;
public async Task GenerateJoke()
{
var result =
await httpClient.GetAsync("https://v2.jokeapi.dev/joke/Programming?type=twopart");
result.EnsureSuccessStatusCode();
var body = await result.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(body)) return;
var joke = JsonSerializer.Deserialize<Joke>(body);
if (joke == null || joke.Error.HasValue && joke.Error.Value) return;
Console.WriteLine(joke.Setup);
Thread.Sleep(TimeSpan.FromSeconds(5));
Console.WriteLine(joke.Delivery);
}
public class Joke
{
[JsonPropertyName("error")]
public bool? Error { get; set; }
[JsonPropertyName("setup")]
public string? Setup { get; set; }
[JsonPropertyName("delivery")]
public string? Delivery { get; set; }
}
}
The class accepts a HttpClient
to call the jokes API. The GenerateJoke
method creates an async request, asking for a two-parted joke. After a bit of error handling, the joke is outputted to the console with a sleep in-between for the user to think about potential punchlines.
Calling the code from the Program
class is straightforward:
public static class Program
{
public static async Task Main()
{
var httpClient = new HttpClient();
var jokes = new Jokes(httpClient);
await jokes.GenerateJoke();
}
}
Here's the tool in action:
What do you get if you lock a monkey in a room with a typewriter for 8 hours?
A regular expression.
We can write a test for the Jokes
class like this:
public class JokesTest
{
[Test]
public async Task CanGenerateJoke()
{
// Arrange
var stringWriter = new StringWriter();
Console.SetOut(stringWriter);
var httpClient = new HttpClient();
var jokes = new Jokes(httpClient);
// Act
await jokes.GenerateJoke();
// Assert
var output = stringWriter.ToString().Trim();
Assert.That(output, Is.Not.Null);
var lines = output.Split(Environment.NewLine).ToList();
Assert.That(lines.Count, Is.EqualTo(2));
Assert.That(lines.Exists(line => string.IsNullOrWhiteSpace(line)), Is.False);
}
}
The test method is implemented using NUnit but it could be any test framework of your choice. The test uses a StringWriter
and the Console.SetOut
method to intercept text written to the console. The three asserts will verify that two lines of text has been outputted to the console.
The downside of the test is that it is an integration test, dependent on the JokeAPI being available. This means that if the API is down, our test will fail. We've finally reached the topic of discussion for this post. How do we mock the HttpClient
to not call the real API?
To intercept an HTTP request made on the HttpClient
we can implement what's called an HTTP message handler. In C#, a message handler is implemented by extending the abstract HttpMessageHandler
class. An HttpMessageHandler
can be used to customize the behavior of HTTP requests and responses. HttpClient
uses HttpMessageHandler
internally to send HTTP requests and receive responses, but you can also provide your own custom HttpMessageHandler
to modify or extend its behavior. Besides the testing scenario presented in this post, a custom HttpMessageHandler
can also be used to:
- Adding custom headers and/or authentication.
- Logging or tracing (like logging errors to elmah.io 😉).
- Handling HTTP errors and/or retries.
Let's create an implementation of a HttpMessageHandler
that returns static JSON:
public class StaticJsonHandler(string json) : HttpMessageHandler
{
private readonly string json = json;
sealed protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(json, Encoding.UTF8, "application/json"),
};
return Task.FromResult(response);
}
}
There's only a single method to override here, the SendAsync
method. The implementation will short-circuit all real HTTP communications and just set the JSON response provided through the constructor.
Changing the test to a unit test now only requires an instance of the new message handler alongside some static JSON:
var joke = new Jokes.Joke
{
Error = false,
Setup = ".NET developers are picky when it comes to food.",
Delivery = "They only like chicken NuGet.",
};
var jokeJson = JsonSerializer.Serialize(joke);
var jsonMessageHandler = new StaticJsonHandler(jokeJson);
var httpClient = new HttpClient(jsonMessageHandler);
The code generates some valid JSON by re-using the Joke
model with some random data. Notice how the StaticJsonHandler
object is provided in the constructor for the HttpClient
.
The assert part can now be turned into more specific checks since we always know the joke outputted to the console:
public class JokesTest
{
[Test]
public async Task CanGenerateJoke()
{
// Arrange
var stringWriter = new StringWriter();
Console.SetOut(stringWriter);
var joke = new Jokes.Joke
{
Error = false,
Setup = ".NET developers are picky when it comes to food.",
Delivery = "They only like chicken NuGet.",
};
var jokeJson = JsonSerializer.Serialize(joke);
var jsonMessageHandler = new StaticJsonHandler(jokeJson);
var httpClient = new HttpClient(jsonMessageHandler);
var jokes = new Jokes(httpClient);
// Act
await jokes.GenerateJoke();
// Assert
var output = stringWriter.ToString().Trim();
Assert.That(output, Is.Not.Null);
var lines = output.Split(Environment.NewLine).ToList();
Assert.That(lines.Count, Is.EqualTo(2));
Assert.That(lines.First(), Is.EqualTo(joke.Setup));
Assert.That(lines.Last(), Is.EqualTo(joke.Delivery));
}
}
Re-using the StaticJsonHandler
we could extend the tests with more tests including strange characters in the response, cancelation of the request, and even simulate that the HTTP request fails to make sure proper error handling is implemented.
HttpClient
without any dependencies. Many mocking frameworks out there offer various levels of support for implementing code like this (maybe check out this post for a comparison of mocking frameworks). There is also mockhttp that implements a HttpMessageHandler
to help writing unit tests similar to the approach taken in this post.