Search code examples
c#unit-testingasp.net-corexunithttpclientfactory

Problem mocking IHttpMessageHandler for both IHttpClientFactory and typed HttpClient


I have some classes that I want to test that receive an IHttpClientFactory. Then they use that factory to create a HttpClient inside:

public class SampleClassUsingHttpClientFactory
{
    private readonly IHttpClientFactory _httpClientFactory;

    public SampleClassUsingHttpClientFactory(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task Run()
    {
        ...
        var client = _httpClientFactory.CreateClient();
        await client.SendAsync(request);
        ...
    }
}

To test this class I mock the HttpClientFactory to always return an HttpClient with the HttpMessageHandler mocked:

public class WebApplicationFactoryWithMockedFactory : WebApplicationFactory<Startup>
{
    public readonly Mock<HttpMessageHandler> HttpMessageHandlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);

    public readonly Mock<IHttpClientFactory> HttpClientFactoryMock = new Mock<IHttpClientFactory>();

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddScoped(srv => HttpMessageHandlerMock.Object);
            services.AddScoped(srv => HttpClientFactoryMock.Object);
        });
    }

    public void ConfigureHttpClientFactoryForMockedHttpClient(string url)
    {
        HttpClientFactoryMock
            .Setup(x => x.CreateClient(It.IsAny<string>()))
            .Returns(new HttpClient(HttpMessageHandlerMock.Object)
            {
                BaseAddress = new Uri(url)
            });
    }

Then I can set up the HttpMessageHandlerMock to behave as I need for the tests. Everything works fine.

On the other side, other classes that I want to test directly receive a typed HttpClient. For example:

public class SampleClassUsingTypedHttpClient : ISampleClassUsingTypedHttpClient
{
    private readonly HttpClient _httpClient;

    public SampleClassUsingTypedHttpClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task Execute()
    {
        ...
        await _httpClient.SendAsync(request);
        ...
    }
}

The proper HttpClient is injected because there is the following registration in the ServiceCollection:

services.AddHttpClient<ISampleClassUsingTypedHttpClient, SampleClassUsingTypedHttpClient>()
    .ConfigureHttpClient((serviceProvider, httpClient) =>
    {
        httpClient.BaseAddress = new Uri("http://this.is.a.sample");
    });

To test SampleClassUsingTypedHttpClient I register the typed HttpMessageHandler forcing to use the HttpMessageHandlerMock as the message handler:

public class WebApplicationFactoryWithTypedHttpClient : WebApplicationFactory<Startup>
{
    public readonly Mock<HttpMessageHandler> HttpMessageHandlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
    
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddHttpClient<ISampleClassUsingTypedHttpClient, SampleClassUsingTypedHttpClient>()
                .ConfigureHttpClient(x => x.BaseAddress = new Uri("http://this.is.a.sample.uri.for.tests"))
                .ConfigurePrimaryHttpMessageHandler(() => HttpMessageHandlerMock.Object);
        });
    }
}

Then I can set up the HttpMessageHandlerMock to behave as I need for the tests and everything is OK.

But some tests need to have registered both mocks (HttpClientFactory and a typed HttpClient):

public class WebApplicationFactoryWithMockedFactoryAndTypedHttpClient : WebApplicationFactory<Startup>
{
    public readonly Mock<HttpMessageHandler> HttpMessageHandlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);

    public readonly Mock<IHttpClientFactory> HttpClientFactoryMock = new Mock<IHttpClientFactory>();

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddScoped(srv => HttpMessageHandlerMock.Object);
            services.AddScoped(srv => HttpClientFactoryMock.Object);
            
            services.AddHttpClient<ISampleClassUsingTypedHttpClient, SampleClassUsingTypedHttpClient>()
                .ConfigureHttpClient(x => x.BaseAddress = new Uri("http://this.is.a.sample.uri.for.tests"))
                .ConfigurePrimaryHttpMessageHandler(() => HttpMessageHandlerMock.Object);           
        });
    }
    
    public void ConfigureHttpClientFactoryForMockedHttpClient(string url)
    {
        HttpClientFactoryMock
            .Setup(x => x.CreateClient(It.IsAny<string>()))
            .Returns(new HttpClient(HttpMessageHandlerMock.Object)
            {
                BaseAddress = new Uri(url)
            });
    }
}

When executing the tests a NullReferenceException is thrown during dependency injection. It seems by the stack trace that .NET is internally using a DefaultTypedHttpClientFactory that is not able to create an instance of HttpClient. Maybe it is calling the mocked HttpClientFactory but the mock returns null, but it shouldn't because it is set up with a call to ConfigureHttpClientFactoryForMockedHttpClient. This is a fragment of the stack trace:

   at Microsoft.Extensions.Http.DefaultTypedHttpClientFactory`1.CreateClient(HttpClient httpClient)
   at Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.<>c__DisplayClass12_0`2.<AddTypedClientCore>b__0(IServiceProvider s)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass1_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
   at Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.<>c__DisplayClass4_0.<CreateActivator>b__0(ControllerContext controllerContext)
   at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.<>c__DisplayClass5_0.<CreateControllerFactory>g__CreateController|0(ControllerContext controllerContext)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
   ...

Is there any way to solve this problem?


Solution

  • I've found the problem in my code. When you mock IHttpClientFactory, like I was doing, then every HttpClient is created using that mocked factory, so this line was doing nothing at all:

    services.AddHttpClient<ISampleClassUsingTypedHttpClient, SampleClassUsingTypedHttpClient>()
        .ConfigureHttpClient(x => x.BaseAddress = new Uri("http://this.is.a.sample.uri.for.tests"))
        .ConfigurePrimaryHttpMessageHandler(() => HttpMessageHandlerMock.Object);
    

    The solution is to setup the mock of the IHttpClientFactory to return also a mocked HttpClient for typed client requests. It can be done with a call to a method like this:

    public void ConfigureHttpClientFactoryMockForMockedTypedHttpClient<T>(Uri baseAddress)
    {
        string httpClientName = typeof(T).Name;
    
        HttpClientFactoryMock
            .Setup(x => x.CreateClient(It.Is<string>(x => x.Equals(httpClientName, StringComparison.InvariantCultureIgnoreCase))))
            .Returns(new HttpClient(HttpMessageHandlerMock.Object)
            {
                BaseAddress = baseAddress
            });
    }