C# – How to unit test code that uses HttpClient

HttpClient is a dependency. Like any other dependency, you need to pass it into your code (aka dependency injection). By passing it in, you can mock it out in 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 /nflteams/getdivision. This returns a list of NFL teams belonging to the specified division. The following example shows a call to this to get the NFC North division.

nflteams/getdivisions endpoint returning the NFC North division

Untested client code

I have the following code that uses HttpClient to do a GET on the /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();
	}
}

Note: This is using Newtonsoft.Json for JSON deserialization.

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);
}

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();
	}
}

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();
	}
}

Add unit test – mock out the wrapper

[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);
}

Note: I am using Moq.

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();
	}
}

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()
[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);
}

Note: I am using Moq.

Leave a Comment