When you want to unit test code that uses HttpClient, you’ll want to treat HttpClient like any other dependency: pass it into the code (aka dependency injection) and then mock it out in the unit tests.
There are two approaches to mocking it out:
- Wrap the HttpClient and mock out the wrapper.
- Use a real HttpClient with a mocked out HttpMessageHandler.
In this article I’ll show examples of these two approaches.
Table of Contents
Untested code that uses HttpClient
To get started here’s the endpoint and the untested client-side code.
Endpoint
I have an endpoint called GET /nflteams/getdivision in my web API that returns a list of NFL teams belonging to the specified division. The following image shows an example of making a request to this endpoint with Postman:
Untested client code
I have the following code that sends a request with HttpClient to the GET /nflteams/getdivision endpoint. This is currently untested. To show the two unit test approaches, I’ll unit test this code.
public class NFLTeamsDataService : IDisposable
{
private readonly HttpClient HttpClient;
private readonly UriBuilder GetDivisionsUri;
public NFLTeamsDataService(HttpClient httpClient, string url)
{
HttpClient = httpClient;
GetDivisionsUri = new UriBuilder($"{url}/nflteams/getdivision");
}
public async Task<List<NFLTeam>> GetDivision(string conference, string division)
{
GetDivisionsUri.Query = $"conference={conference}&division={division}";
var response = await HttpClient.GetAsync(GetDivisionsUri.ToString());
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<NFLTeam>>(json);
}
public void Dispose()
{
HttpClient?.Dispose();
}
}
Code language: C# (cs)
Note: This checks the response status code, then deserializes the response JSON string. Alternatively, you can use a deserialize the JSON response stream (instead of string) or use the high-level HttpClient.GetFromJsonAsync() instead.
Approach 1 – Wrap the HttpClient and mock the wrapper
HttpClient doesn’t implement an interface so it can’t be mocked out. Instead, I have to create a wrapper class. It’ll contain a HttpClient instance and wrap the methods I’m using.
Create a wrapper interface
public interface IHttpClientWrapper : IDisposable
{
Task<HttpResponseMessage> GetAsync(string url);
}
Code language: C# (cs)
Implement the wrapper
public class HttpClientWrapper : IHttpClientWrapper
{
private readonly HttpClient HttpClient;
public HttpClientWrapper()
{
HttpClient = new HttpClient();
}
public async Task<HttpResponseMessage> GetAsync(string url)
{
return await HttpClient.GetAsync(url);
}
public void Dispose()
{
HttpClient?.Dispose();
}
}
Code language: C# (cs)
Pass in the wrapper
public class NFLTeamsDataService : IDisposable
{
private readonly IHttpClientWrapper HttpClient;
private readonly UriBuilder GetDivisionsUri;
public NFLTeamsDataService(IHttpClientWrapper httpClient, string url)
{
HttpClient = httpClient;
GetDivisionsUri = new UriBuilder($"{url}/nflteams/getdivision");
}
public async Task<List<NFLTeam>> GetDivision(string conference, string division)
{
GetDivisionsUri.Query = $"conference={conference}&division={division}";
var response = await HttpClient.GetAsync(GetDivisionsUri.ToString());
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<NFLTeam>>(json);
}
public void Dispose()
{
HttpClient?.Dispose();
}
}
Code language: C# (cs)
Add unit test – mock out the wrapper
using Moq;
[TestMethod()]
public async Task GetDivisionTest()
{
//arrange
var expectedTeamList = new List<NFLTeam>
{
new NFLTeam() { Team="Detroit Lions", Conference="NFC", Division="North"},
new NFLTeam() { Team="Chicago Bears", Conference="NFC", Division="North"},
new NFLTeam() { Team="Minnesota Vikings", Conference="NFC", Division="North"},
new NFLTeam() { Team="Green Bay Packers", Conference="NFC", Division="North"},
};
var json = JsonConvert.SerializeObject(expectedTeamList);
string url = "http://localhost:1234";
HttpResponseMessage httpResponse = new HttpResponseMessage();
httpResponse.StatusCode = System.Net.HttpStatusCode.OK;
httpResponse.Content = new StringContent(json);
var mockHttpClientWrapper = new Mock<IHttpClientWrapper>();
mockHttpClientWrapper.Setup(t => t.GetAsync(It.Is<string>(s=>s.StartsWith(url))))
.ReturnsAsync(httpResponse);
NFLTeamsDataService service = new NFLTeamsDataService(mockHttpClientWrapper.Object, url);
//act
var actualTeamList = await service.GetDivision("NFC", "North");
//assert
CollectionAssert.AreEquivalent(expectedTeamList, actualTeamList);
}
Code language: C# (cs)
Note: This is using the Moq mocking library.
Approach 2 – Pass in the real HttpClient and mock out the HttpMessageHandler
In this approach I’m passing in the actual HttpClient, but mocking out its HttpMessageHandler. This is an abstract class so it can be mocked.
No change needed to NFLTeamsDataService
I’m already passing in the HttpClient to my code, so there is no change needed.
public class NFLTeamsDataService : IDisposable
{
private readonly HttpClient HttpClient;
private readonly UriBuilder GetDivisionsUri;
public NFLTeamsDataService(HttpClient httpClient, string url)
{
HttpClient = httpClient;
GetDivisionsUri = new UriBuilder($"{url}/nflteams/getdivision");
}
public async Task<List<NFLTeam>> GetDivision(string conference, string division)
{
GetDivisionsUri.Query = $"conference={conference}&division={division}";
var response = await HttpClient.GetAsync(GetDivisionsUri.ToString());
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<NFLTeam>>(json);
}
public void Dispose()
{
HttpClient?.Dispose();
}
}
Code language: C# (cs)
Add unit test – mock out HttpMessageHandler
The HttpMessageHandler class is abstract and has a protected method called SendAsync(). I want to mock out SendAsync(), so that when a GET is called on the passed in URL, it returns my HttpResponseMessage.
Because this is a protected method, I need to use a special mocking approach:
- Call Protected().
- Call Setup() – matching the signature of HttpResponseMessage.SendAsync(), and using a string to specify the method name.
- Use ItExpr() instead of It() when specifying the method signature in Setup()
using Moq;
[TestMethod()]
public async Task GetDivisionTest()
{
//arrange
var expectedTeamList = new List<NFLTeam>
{
new NFLTeam() { Team="Detroit Lions", Conference="NFC", Division="North"},
new NFLTeam() { Team="Chicago Bears", Conference="NFC", Division="North"},
new NFLTeam() { Team="Minnesota Vikings", Conference="NFC", Division="North"},
new NFLTeam() { Team="Green Bay Packers", Conference="NFC", Division="North"},
};
var json = JsonConvert.SerializeObject(expectedTeamList);
string url = "http://localhost:1234";
HttpResponseMessage httpResponse = new HttpResponseMessage();
httpResponse.StatusCode = System.Net.HttpStatusCode.OK;
httpResponse.Content = new StringContent(json, Encoding.UTF8, "application/json");
Mock<HttpMessageHandler> mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.Is<HttpRequestMessage>(r=>r.Method == HttpMethod.Get && r.RequestUri.ToString().StartsWith(url)),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(httpResponse);
HttpClient httpClient = new HttpClient(mockHandler.Object);
NFLTeamsDataService service = new NFLTeamsDataService(httpClient, url);
//act
var actualTeamList = await service.GetDivision("NFC", "North");
//assert
CollectionAssert.AreEquivalent(expectedTeamList, actualTeamList);
}
Code language: C# (cs)
Comments are closed.