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?
CodePudding user response:
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
});
}