It's pretty common practice in .NET Core to take a dependency on HttpClient in your constructor and using the built-in DI container extension to register this.
When it comes to unit testing it can always be a bit fiddly when you depend on a concrete class rather than an interface. After solving this problem several times when it comes to HttpClient based unit tests I've create a simple TestHttpClient and TestHttpClientBuilder to simplify the process:
public class TestHttpClientBuilder
{
private readonly HttpResponseMessage _stubHttpResponseMessage = new(HttpStatusCode.OK);
private Exception? _exception;
public TestHttpClientBuilder WithStatusCode(HttpStatusCode statusCode)
{
_stubHttpResponseMessage.StatusCode = statusCode;
return this;
}
public TestHttpClientBuilder WithJsonContent<T>(T expectedResponseObject)
{
_stubHttpResponseMessage.Content = new StringContent(JsonConvert.SerializeObject(expectedResponseObject), Encoding.UTF8, "application/json");
return this;
}
public TestHttpClientBuilder WithException(Exception ex)
{
_exception = ex;
return this;
}
public TestHttpClient Build()
{
return new TestHttpClient(
_exception != null ?
new FakeHttpMessageHandler(_exception) :
new FakeHttpMessageHandler(_stubHttpResponseMessage));
}
public class TestHttpClient : HttpClient
{
private readonly FakeHttpMessageHandler _httpMessageHandler;
internal TestHttpClient(FakeHttpMessageHandler httpMessageHandler) : base(httpMessageHandler)
{
_httpMessageHandler = httpMessageHandler;
BaseAddress = new Uri("http://localhost.com");
}
public IReadOnlyList<HttpRequestMessage> CapturedRequests => _httpMessageHandler.CapturedRequests;
}
}
public class FakeHttpMessageHandler : HttpMessageHandler
{
private readonly Exception? _exception;
private readonly HttpResponseMessage _response = new();
private readonly List<HttpRequestMessage> _capturedRequests = new List<HttpRequestMessage>();
public FakeHttpMessageHandler(Exception exception)
{
_exception = exception;
}
public FakeHttpMessageHandler(HttpResponseMessage response)
{
_response = response;
}
public IReadOnlyList<HttpRequestMessage> CapturedRequests => _capturedRequests;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
_capturedRequests.Add(request);
if (_exception != null)
{
throw _exception;
}
return Task.FromResult(_response);
}
}
Given this code is available to your unit tests, you can now use the builder when instantiating the SUT and use the builder methods to configure the possible responses and/or inspect the captured requests to test your outbound calls.
e.g.
public class UnitTestClass
{
private TestHttpClientBuilder? _testHttpClientBuilder;
private Lazy<TestHttpClientBuilder.TestHttpClient>? _testHttpClient;
public void SetUp()
{
_testHttpClientBuilder = new TestHttpClientBuilder()
.WithStatusCode(HttpStatusCode.OK)
.WithJsonContent(new MyDataType());
_testHttpClient = new Lazy<TestHttpClientBuilder.TestHttpClient>(() => _testHttpClientBuilder.Build());
}
}