
Unit Testing HTTPClient in dotnet
HTTP Client
6 Articles
Table of Contents
What we gonna do?
In this article, let's learn about how to Unit Test HTTPClient in .NET.
Note: If you have not done so already, I recommend you read the article on Extending HTTPClient with Custom Http Message Handlers in dotnet.
There are multiple ways to Unit Test HTTP Client in dotnet. Most of the examples you will find on the internet will be using Moq or NSubstitute. But I prefer to write Custom Http Message Handlers to Unit Test HTTP Client in dotnet.
Why we gonna do?
You're working on a part of your application that relies on HttpClient to make API calls. Testing this can become cumbersome, as you don't want to actually call the API. Because it's not an integration test. Moreover, constantly calling a service that's cloud hosted can cost you quite a bit of money. You just want to test one specific piece of functionality that happens to rely on data being returned from an HTTP call.
Integrating with a web API is a common task required by many applications. Http Client is a class that simplifies the consumption of web APIs. It provides a base class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI. The HttpClient class is typically used to send and receive requests from a web service by using the HTTP protocol.
Most of the time I see developers including me say we can test HTTP Client or calls in Integration Test but Unit testing allows developers to verify the behavior and logic of individual units of code in isolation quickly. By writing tests, you can ensure that your code functions correctly and remains reliable even after making changes or adding new features. Unit tests are faster and help catch bugs early in the development process and provide confidence in the correctness of the code.
Note: If you have not done so already, I recommend you read the article on Implementing TDD in C# .Net.
How we gonna do?
Take error handling, for example. You want to ensure that when the API responds with a dreaded 401 Unauthorized status code that your application handles it gracefully. You don't want to actually call the API, though. You just want to ensure that your application handles the response correctly. This is where Custom Http Message Handlers come in handy. Well, we can write a custom handler to stop communication with the API and return the response required for our test. For our use case, we don't want to communicate with the API, but we do want to get back a 401 Unauthorized response.
The following code shown a simple implementation of a custom message handler that returns a 401 Unauthorized response. Notice that we are using HttpMessageHandler instead of DelegatingHandler. This is because we don't want to pass the request to the next handler in the pipeline. We want to return the response we want. So, we are using HttpMessageHandler to short circuit it.
public class Return401UnauthorizedResponseHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
return Task.FromResult(response);
}
}
Now, we can use this custom message handler to unit test our HTTP client. The following code shows a simple implementation of a unit test for HTTP client. All we need to do is to pass our custom message handler to the HTTP client and make the HTTP call. The HTTP call will be intercepted by our custom message handler and will return the response we want which is 401 Unauthorized Status Code in our case. This will help you to assert your code against the response you want.
[Fact]
public async Task GetData_On401Response_MustThrowUnauthorizedApiAccessException()
{
var httpClient = new HttpClient(new Return401UnauthorizedResponseHandler())
{
BaseAddress = new Uri("http://ilovedotnet.org")
};
var apiAccessClass = new ApiAccess(httpClient);
await Assert.ThrowsAsync<UnauthorizedApiAccessException>(() => apiAccessClass.GetDataAsync(CancellationToken.None));
}
Here is another similar example for 200 Success Status Code:
public class Return200OkResponseHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(new { Name = "I Love .NET" }))
};
return Task.FromResult(response);
}
}
And here comes the corresponding unit test.
[Fact]
public async Task GetData_On200Response_MustReturnExpectedData()
{
var httpClient = new HttpClient(new Return200OkResponseHandler())
{
BaseAddress = new Uri("http://ilovedotnet.org")
};
var apiAccessClass = new ApiAccess(httpClient);
var result = await apiAccessClass.Get();
Assert.Equal(200, result.StatusCode);
Assert.Equal("I Love .NET", result.Name);
}
What if now you have a requirement to get different response for a success status code based on input? For example, you want to return Apple as response if the input contains apple in query string and return Orange as response if the input contains orange in query string.
We can achieve this by using a custom message handler that checks the request and returns the response accordingly.
public class Data
{
public static List<Product> Products =
[
new Product { Name = "Apple", Price = 8.43m },
new Product { Name = "Orange", Price = 2.52m },
new Product { Name = "Banana", Price = 4.99m },
new Product { Name = "Mango", Price = 4.68m },
new Product { Name = "Grapes", Price = 9.98m }
];
}
public class Return200OkResponseHandler() : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var productName = request.RequestUri?.Segments.LastOrDefault("apple").Replace(".json", string.Empty);
var product = Data.Products.SingleOrDefault(x => x.Name.ToLower().Equals(productName, StringComparison.OrdinalIgnoreCase));
var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(product))
};
return Task.FromResult(response);
}
}
See how I'm reading the request and returning the response based on the request from an in memory data. This is a simple example but you can extend it to handle more complex scenarios.
The only drawback of this approach is that you need to write a custom message handler for each status code you want to test. I feel like we will also need to maintain these custom message handlers. But this is the best approach I found to unit test HTTP client in dotnet when there is no option available to mock the HTTP client.
Summary
In this article, we learned about how to Unit Test HTTP Client in dotnet. We also learned about why we should write unit tests and what is an HTTP client. We also learned about how to write custom HTTP message handlers to unit test HTTP client in dotnet. I hope you enjoyed reading this article. With this, I'm concluding this HTTP Client series. Please share this with your network and help other dotnet devs.