I'm not convinced by many of the answers.
First of all, imagine you want to unit test a method that uses HttpClient. You should not instantiate HttpClient directly in your implementation. You should inject a factory with the responsibility of providing an instance of HttpClient for you. That way you can mock later on that factory and return whichever HttpClient you want (e.g: a mock HttpClient and not the real one).
So, you would have a factory like the following:
public interface IHttpClientFactory { HttpClient Create(); }
And an implementation:
public class HttpClientFactory : IHttpClientFactory { public HttpClient Create() { var httpClient = new HttpClient(); return httpClient; } }
Of course you would need to register in your IoC Container this implementation. If you use Autofac it would be something like:
builder .RegisterType<IHttpClientFactory>() .As<HttpClientFactory>() .SingleInstance();
Now you would have a proper and testeable implementation. Imagine that your method is something like:
public class MyHttpClient : IMyHttpClient { private readonly IHttpClientFactory _httpClientFactory; public SalesOrderHttpClient(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async Task<string> PostAsync(Uri uri, string content) { using (var client = _httpClientFactory.Create()) { var clientAddress = uri.GetLeftPart(UriPartial.Authority); client.BaseAddress = new Uri(clientAddress); var content = new StringContent(content, Encoding.UTF8, "application/json"); var uriAbsolutePath = uri.AbsolutePath; var response = await client.PostAsync(uriAbsolutePath, content); var responseJson = response.Content.ReadAsStringAsync().Result; return responseJson; } } }
Now the testing part. HttpClient extends HttpMessageHandler, which is abstract. Let's create a "mock" of HttpMessageHandler that accepts a delegate so that when we use the mock we can also setup each behaviour for each test.
public class MockHttpMessageHandler : HttpMessageHandler { private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc; public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc) { _sendAsyncFunc = sendAsyncFunc; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return await _sendAsyncFunc.Invoke(request, cancellationToken); } }
And now, and with the help of Moq (and FluentAssertions, a library that makes unit tests more readable), we have everything needed to unit test our method PostAsync that uses HttpClient
public static class PostAsyncTests { public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async : Given_WhenAsync_Then_Test { private SalesOrderHttpClient _sut; private Uri _uri; private string _content; private string _expectedResult; private string _result; protected override void Given() { _uri = new Uri("http://test.com/api/resources"); _content = "{\"foo\": \"bar\"}"; _expectedResult = "{\"result\": \"ok\"}"; var httpClientFactoryMock = new Mock<IHttpClientFactory>(); var messageHandlerMock = new MockHttpMessageHandler((request, cancellation) => { var responseMessage = new HttpResponseMessage(HttpStatusCode.Created) { Content = new StringContent("{\"result\": \"ok\"}") }; var result = Task.FromResult(responseMessage); return result; }); var httpClient = new HttpClient(messageHandlerMock); httpClientFactoryMock .Setup(x => x.Create()) .Returns(httpClient); var httpClientFactory = httpClientFactoryMock.Object; _sut = new SalesOrderHttpClient(httpClientFactory); } protected override async Task WhenAsync() { _result = await _sut.PostAsync(_uri, _content); } [Fact] public void Then_It_Should_Return_A_Valid_JsonMessage() { _result.Should().BeEquivalentTo(_expectedResult); } } }
Obviously this test is silly, and we're really testing our mock. But you get the idea. You should test meaningful logic depending on your implementation such as..
- if the code status of the response is not 201, should it throw an exception?
- if the response text cannot be parsed, what should happen?
- etc.
The purpose of this answer was to test something that uses HttpClient and this is a nice clean way to do so.
UPDATE Lately I use an http builder in my tests where I can easily inject the json response I expect.
public class HttpClientBuilder { private HttpMessageHandler _httpMessageHandler = new HttpClientHandler(); public HttpClientBuilder WithJsonResponse(HttpStatusCode httpStatusCode, string json, string contentType = "application/json") { var mockHttpMessageHandler = new MockHttpMessageHandler( (request, cancellation) => { var responseMessage = new HttpResponseMessage(httpStatusCode) { Content = new StringContent(json, Encoding.UTF8, contentType) }; var result = Task.FromResult(responseMessage); return result; }); _httpMessageHandler = mockHttpMessageHandler; return this; } public HttpClient Build() { var httpClient = new HttpClient(_httpMessageHandler); return httpClient; } } class MockHttpMessageHandler : HttpMessageHandler { private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc; public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc) { _sendAsyncFunc = sendAsyncFunc; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return await _sendAsyncFunc.Invoke(request, cancellationToken); } }
so, as long as I have the HttpClient behind an abstraction like IHttpClientFactory, as I've suggested above, in my tests I can do something like
var httpClientFactoryMock = new Mock<IHttpClientFactory>(); var jsonResponse = "{\"hello world\"}"; var httpClient = new HttpClientBuilder() .WithJsonResponse(HttpStatusCode.OK, jsonResponse) .Build(); httpClientFactoryMock .Setup(x => x.Create()) .Returns(httpClient); var httpClientFactory = httpClientFactoryMock.Object;
and then use that httpClientFactory.
HttpClientin your interface is where the problem is. You are forcing your client to use theHttpClientconcrete class. Instead, you should expose an abstraction of theHttpClient.