C# - How to unit test code that uses HttpClient | makolyte (2024)

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:

C# - How to unit test code that uses HttpClient | makolyte (1)

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(){//arrangevar 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);//actvar actualTeamList = await service.GetDivision("NFC", "North");//assertCollectionAssert.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(){//arrangevar 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);//actvar actualTeamList = await service.GetDivision("NFC", "North");//assertCollectionAssert.AreEquivalent(expectedTeamList, actualTeamList);}Code language: C# (cs)

Related Articles

  • C# – How to add request headers when using HttpClient
  • C# – How to change the HttpClient timeout per request
  • C# – Configuring HttpClient connection keep-alive
  • C# – How to send synchronous requests with HttpClient
  • C# – Unit testing code that does File IO
C# - How to unit test code that uses HttpClient | makolyte (2024)

References

Top Articles
Latest Posts
Article information

Author: Dr. Pierre Goyette

Last Updated:

Views: 6296

Rating: 5 / 5 (70 voted)

Reviews: 93% of readers found this page helpful

Author information

Name: Dr. Pierre Goyette

Birthday: 1998-01-29

Address: Apt. 611 3357 Yong Plain, West Audra, IL 70053

Phone: +5819954278378

Job: Construction Director

Hobby: Embroidery, Creative writing, Shopping, Driving, Stand-up comedy, Coffee roasting, Scrapbooking

Introduction: My name is Dr. Pierre Goyette, I am a enchanting, powerful, jolly, rich, graceful, colorful, zany person who loves writing and wants to share my knowledge and understanding with you.