There has been a lot of discussion about how and even if the HttpClient
class is testable. And it very much is.
So I wanted to write a quick post giving you three options that you can use when you need to write tests involving the HttpClient
.
Let’s assume we have a simple class which gets a list of songs from an API. I’ll use this as the example class we wish to test.
public class SongService
{
private readonly HttpClient _httpClient;
public SongService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<Song>> GetSongs()
{
var requestUri = "http://api.songs.com/songs";
var response = await _httpClient.GetAsync(requestUri);
var responseData = response.Content.ReadAsString();
var songs = JsonConvert.DeserializeObject<List<Song>>(responseData);
return songs;
}
}
1. Wrapping HttpClient
Wrapping the HttpClient will give us the ability to have a interface. In turn we can update the Songs
class to expect that interface.
This will give us two benefits. Firstly the SongService
will be improved as it will be depending on an interface instead of a concrete class. Secondly, we can then mock that interface to make the SongService
easier to test.
IHttpProvider.cs
public interface IHttpProvider
{
Task<HttpResponseMessage> GetAsync(string requestUri);
Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content);
Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content);
Task<HttpResponseMessage> DeleteAsync(string requestUri);
}
HttpProvider.cs
public class HttpClientProvider : IHttpClientProvider
{
private readonly HttpClient _httpClient;
public HttpClientProvider(HttpClient httpClient)
{
_httpClient = httpClient;
}
public Task<HttpResponseMessage> GetAsync(string requestUri)
{
return _httpClient.GetAsync(requestUri);
}
public Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content)
{
return _httpClient.PostAsync(requestUri, content);
}
public Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content)
{
return _httpClient.PutAsync(requestUri, content);
}
public Task<HttpResponseMessage> DeleteAsync(string requestUri)
{
return _httpClient.DeleteAsync(requestUri);
}
}
I’ve wrapped the four main actions of the HttpClient
. But you may want to expose more or less in your implementation. For example, if you only need the GetAsync
method then just do the following.
IHttpProvider.cs
public interface IHttpProvider
{
Task<HttpResponseMessage> GetAsync(string requestUri);
}
HttpProvider.cs
public class HttpClientProvider : IHttpClientProvider
{
private readonly HttpClient _httpClient;
public HttpClientProvider(HttpClient httpClient)
{
_httpClient = httpClient;
}
public Task<HttpResponseMessage> GetAsync(string requestUri)
{
return _httpClient.GetAsync(requestUri);
}
}
Now we can change the SongService
to accept our new interface like so.
Songs.cs
public class SongService
{
private readonly IHttpProvider _httpProvider;
public SongService(IHttpProvider httpProvider)
{
_httpProvider = httpProvider;
}
public async Task<List<Song>> GetSongsAsync()
{
var requestUri = "http://api.songs.com/songs";
var response = await _httpProvider.GetAsync(requestUri);
var responseData = response.Content.ReadAsString();
var songs = JsonConvert.DeserializeObject<List<Song>>(responseData);
return songs;
}
}
We can now test the SongService
really easily as we have an interface we can mock.
2. Fake HttpMessageHandler
The HttpClient
itself is actually an abstraction layer. It is the HttpMessageHandler
which determines how to send requests.
The good news from a testing point of view. Is that the HttpClient
has a constructor which takes a single argument of type HttpMessageHandler
. Which means we can create a fake handler setup how we want and pass it in for testing.
FakeHttpMessageHandler.cs
public class FakeHttpMessageHandler : HttpMessageHandler
{
public virtual HttpResponseMessage Send(HttpRequestMessage request)
{
// Configure this method however you wish for your testing needs.
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(Send(request));
}
}
AwesomeUnitTests.cs
[Test]
public void AwesomeUnitTest1()
{
// Arrange
var fakeHttpMessageHandler = new Mock<FakeHttpMessageHandler> { CallBase = true };
fakeHttpMessageHandler.Setup(x => x.Send(It.IsAny<HttpRequestMessage>()))
.Returns(new HttpResponseMessage(HttpStatusCode.OK));
var fakeHttpClient = new HttpClient(fakeHttpMessageHandler.Object);
// Act
...
// Assert
...
}
Using this method we could test the SongService
without needing to change any of the code. However we are stuck with our FakeHttpMessageHandler
class.
3. Mocking HttpMessageHandler
I have seen this method used a few times but only using the Moq framework.
Moq has an extension method Protected()
which you can access by importing the Moq.Protected
namespace. What the Protected
extension method does is allows you to mock non-public protected members.
This is useful to us as the HttpMessageHandler
’s SendAsync
methods scope is protected internal
. Which means we can do the following.
AwesomeUnitTests.cs
[Test]
public async Task AwesomeUnitTest2()
{
// Arrange
var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
// Act
...
// Assert
...
}
Once again, we would now be able to test the SongService
without having to make any code changes. But this time we are not left with an extra test class.
The slight downside to this method is that we have to use a magic string to declare the method we want to mock. But this could easily be improved by using a string constant instead.
Conclusion
So, there are three different ways which you can test a class consuming the HttpClient
. Hopefully at least one of them will be useful to you.
Whats your preferred method of dealing HttpClient
in unit tests? Also if you have any questions or comments please leave them below.