ihttpclientFactory和键入的httpclient嘲笑IHTTPMESSAGEHANDLER的问题
我有一些我想测试的类,这些课程接收ihttpclientfactory
。然后,他们使用该工厂创建httpclient
内部:
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);
...
}
}
要测试此类,我模拟了httpclientfactory
始终返回httpclient
httpmessagehandler 模拟:
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)
});
}
然后,我可以设置httpmessagehandlermock
在我需要测试时表现出来。一切都很好。
另一方面,我要测试的其他类直接接收一个键入的httpclient
。例如:
public class SampleClassUsingTypedHttpClient : ISampleClassUsingTypedHttpClient
{
private readonly HttpClient _httpClient;
public SampleClassUsingTypedHttpClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task Execute()
{
...
await _httpClient.SendAsync(request);
...
}
}
注入正确的httpclient
,因为serviceCollection中有以下
services.AddHttpClient<ISampleClassUsingTypedHttpClient, SampleClassUsingTypedHttpClient>()
.ConfigureHttpClient((serviceProvider, httpClient) =>
{
httpClient.BaseAddress = new Uri("http://this.is.a.sample");
});
注册强迫使用httpmessagehandlermock
作为消息处理程序:
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);
});
}
}
然后,我可以设置httpmessagehandlermock
以我需要测试,一切都可以。
但是,有些测试需要注册两个模拟(httpclientFactory
和键入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)
});
}
}
执行测试时,nullReferenceException
是在依赖性期间投掷的。通过堆栈跟踪,.NET在内部使用defaultTypedhttpClientFactory
无法创建httpclient
的实例。也许它正在调用模拟的httpclientfactory
,但模拟返回null
,但不应该是因为它是用调用configurehttpclientFactoryFormockedhtttpClclient
设置的。这是堆栈跟踪的片段:
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()
...
有什么方法可以解决此问题?
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?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
我在代码中找到了问题。
httpclient
都创建当您模拟
ihttpclientFactory
,就像我在做的那样,使用该模拟工厂的每个ihttpclientFactory
还返回了一个模拟的httpclient
for键入客户端请求。可以通过调用这样的方法来完成:I've found the problem in my code. When you mock
IHttpClientFactory
, like I was doing, then everyHttpClient
is created using that mocked factory, so this line was doing nothing at all:The solution is to setup the mock of the
IHttpClientFactory
to return also a mockedHttpClient
for typed client requests. It can be done with a call to a method like this: