Previously I showed how to log all HttpClient outgoing requests. I later added some logics for adding default headers in the SendAsync method in OutgoingHttpClient. I wanted to unit test this to make sure my logic is correct. I thought I could mock the HttpClient the normal way but since the SendAsync method in HttpClient is not virtual, I get the following error:
Message: System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member: x => x.SendAsync(It.IsAny<HttpRequestMessage>())
This is what my test looks like
[TestFixture] public class OutgoingHttpClientTests { private Mock<HttpClient> _httpClient; private Mock<IHttpClientProvider> _httpClientProvider; private IOutgoingHttpClient _externalHttpClient; private const string CorrelationIdKey = "x-correlation-id"; [SetUp] public void SetUp() { _httpClient = new Mock<HttpClient>(); _httpClientProvider = new Mock<IHttpClientProvider>(); _httpClientProvider.Setup(x => x.CreateHttpClient(OutgoingHttpClient.HttpClientName)).Returns(_httpClient.Object); _externalHttpClient = new OutgoingHttpClient(_httpClientProvider.Object); } [Test] public async Task GetHttpClient_Should_Add_Correlation_Id_To_Headers() { // Arrange var request = new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = new Uri($"https://www.{Guid.NewGuid()}.com/") }; var payloadString = string.Empty; var sentRequest = new HttpRequestMessage(); _httpClient.Setup(x => x.SendAsync(It.IsAny<HttpRequestMessage>())) .Callback<HttpRequestMessage>((requestParam) => sentRequest = requestParam) .ReturnsAsync(new HttpResponseMessage()); // Act var response = await _externalHttpClient.SendAsync(request); // Assert sentRequest.Headers.Contains(CorrelationIdKey).Should().BeTrue(); } }
To fix the above error, we need to add a fake HttpMessageHandler, have the SendAsync to call some virtual method which allows us to mock and use it as the inner handler of the mock HttpClient.
The fake HttpMessageHandler looks like
public class FakeHttpMessageHandler : HttpMessageHandler { public virtual HttpResponseMessage Send(HttpRequestMessage request) { throw new NotImplementedException("Now we can setup this method in the unit tests since its virtual"); } protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return Task.FromResult(Send(request)); } }
And the test now looks like
[TestFixture] public class OutgoingHttpClientTests { private Mock<HttpClient> _httpClient; private Mock<FakeHttpMessageHandler> _fakeHttpMessageHandler; private Mock<IHttpClientProvider> _httpClientProvider; private IOutgoingHttpClient _externalHttpClient; private const string CorrelationIdKey = "x-correlation-id"; [SetUp] public void SetUp() { _fakeHttpMessageHandler = new Mock<FakeHttpMessageHandler> { CallBase = true }; _httpClient = new Mock<HttpClient>(_fakeHttpMessageHandler.Object); // Set the FakeHttpMessageHandler as HttpClient's inner handler _httpClientProvider = new Mock<IHttpClientProvider>(); _httpClientProvider.Setup(x => x.CreateHttpClient(OutgoingHttpClient.HttpClientName)).Returns(_httpClient.Object); _externalHttpClient = new OutgoingHttpClient(_httpClientProvider.Object); } [Test] public async Task GetHttpClient_Should_Add_Correlation_Id_To_Headers() { // Arrange var request = new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = new Uri($"https://www.{Guid.NewGuid()}.com/") }; var payloadString = string.Empty; var sentRequest = new HttpRequestMessage(); _fakeHttpMessageHandler.Setup(x => x.Send(It.IsAny<HttpRequestMessage>())) .Callback<HttpRequestMessage>((requestParam) => sentRequest = requestParam) .Returns(new HttpResponseMessage()); // Act var response = await _externalHttpClient.SendAsync(request); // Assert sentRequest.Headers.Contains(CorrelationIdKey).Should().BeTrue(); } }
This allows you to assert the HttpRequestMessage that is passed to the SendAsync method in HttpClient and your test does not actually need to make a real HTTP request.