ihttpclientFactory和键入的httpclient嘲笑IHTTPMESSAGEHANDLER的问题

发布于 2025-02-03 03:06:29 字数 8044 浏览 1 评论 0原文

我有一些我想测试的类,这些课程接收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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

方圜几里 2025-02-10 03:06:29

我在代码中找到了问题。 httpclient都创建

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

当您模拟ihttpclientFactory,就像我在做的那样,使用该模拟工厂的每个 ihttpclientFactory还返回了一个模拟的httpclient for键入客户端请求。可以通过调用这样的方法来完成:

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
        });
}

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
        });
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文