C# – How to unit test code that uses HttpClient

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.

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:

nflteams/getdivisions endpoint returning the NFC North division

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.