为 IHttpClientFactory 和类型化 HttpClient 模拟 IHttpMessageHandler 时出现问题
Problem mocking IHttpMessageHandler for both IHttpClientFactory and typed HttpClient
我想测试一些 class 收到 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);
...
}
}
为了测试这个 class 我模拟 HttpClientFactory
总是 return 和 HttpMessageHandler
模拟的 HttpClient
:
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
以按照我需要的方式运行测试。一切正常。
另一边,我想测试的其他 classes 直接收到一个类型化的 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");
});
为了测试 SampleClassUsingTypedHttpClient
我注册了类型化的 HttpMessageHandler
强制使用 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
但模拟 returns null
,但它不应该,因为它是通过调用 ConfigureHttpClientFactoryForMockedHttpClient
设置的。这是堆栈跟踪的片段:
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()
...
有什么办法可以解决这个问题吗?
我在我的代码中发现了问题。当你像我一样模拟 IHttpClientFactory
时,每个 HttpClient
都是使用那个模拟工厂创建的,所以这一行什么都不做:
services.AddHttpClient<ISampleClassUsingTypedHttpClient, SampleClassUsingTypedHttpClient>()
.ConfigureHttpClient(x => x.BaseAddress = new Uri("http://this.is.a.sample.uri.for.tests"))
.ConfigurePrimaryHttpMessageHandler(() => HttpMessageHandlerMock.Object);
解决方案是将 IHttpClientFactory
的模拟设置为 return,同时为类型化的客户端请求设置一个模拟的 HttpClient
。可以通过调用这样的方法来完成:
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
});
}
我想测试一些 class 收到 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);
...
}
}
为了测试这个 class 我模拟 HttpClientFactory
总是 return 和 HttpMessageHandler
模拟的 HttpClient
:
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
以按照我需要的方式运行测试。一切正常。
另一边,我想测试的其他 classes 直接收到一个类型化的 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");
});
为了测试 SampleClassUsingTypedHttpClient
我注册了类型化的 HttpMessageHandler
强制使用 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
但模拟 returns null
,但它不应该,因为它是通过调用 ConfigureHttpClientFactoryForMockedHttpClient
设置的。这是堆栈跟踪的片段:
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()
...
有什么办法可以解决这个问题吗?
我在我的代码中发现了问题。当你像我一样模拟 IHttpClientFactory
时,每个 HttpClient
都是使用那个模拟工厂创建的,所以这一行什么都不做:
services.AddHttpClient<ISampleClassUsingTypedHttpClient, SampleClassUsingTypedHttpClient>()
.ConfigureHttpClient(x => x.BaseAddress = new Uri("http://this.is.a.sample.uri.for.tests"))
.ConfigurePrimaryHttpMessageHandler(() => HttpMessageHandlerMock.Object);
解决方案是将 IHttpClientFactory
的模拟设置为 return,同时为类型化的客户端请求设置一个模拟的 HttpClient
。可以通过调用这样的方法来完成:
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
});
}