如何对配置Microsoft.Extensions.Http.Resilience的类型化HttpClient做单元测试
测试带有Microsoft.Extensions.Http.Resilience弹性策略的HttpClient客户端
你的问题核心是:AddStandardResilienceHandler是IHttpClientBuilder的扩展方法,并非ResiliencePipelineBuilder的成员,所以直接用后者找不到这个方法。下面提供两种可行的测试方案:
方案一:用依赖注入容器构建客户端(最贴近生产配置)
这种方式直接复用项目里的DI配置逻辑,能拿到和生产环境完全一致的、带标准弹性策略的客户端实例,还能方便模拟Http响应:
[Fact] public async Task MyClient_WithStandardResilience_WorksAsExpected() { // 构建测试用DI容器 using var host = Host.CreateDefaultBuilder() .ConfigureServices(services => { // 完全复用生产环境的客户端配置 services.AddHttpClient<IMyClient, MyClient>() .AddStandardResilienceHandler(); // 替换HttpMessageHandler来模拟接口响应(避免真实请求) services.PostConfigure<HttpClientFactoryOptions>(nameof(MyClient), options => { options.HttpMessageHandlerBuilderActions.Add(builder => { var mockHandler = new Mock<HttpMessageHandler>(); mockHandler.Protected() .Setup<Task<HttpResponseMessage>>( "SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{\"data\": 123}") }); builder.PrimaryHandler = mockHandler.Object; }); }); }) .Build(); // 从容器中获取配置好的客户端 var sut = host.Services.GetRequiredService<IMyClient>(); // 执行测试 var result = await sut.GetData(); // 断言结果 Assert.Equal(123, result); }
方案二:手动构建等价的弹性策略管道
AddStandardResilienceHandler本质是封装了一组默认的Http弹性策略(重试、断路器、超时、舱壁、降级),你可以手动构建这组策略的管道,包裹客户端的Http请求:
[Fact] public async Task MyClient_WithManualResiliencePipeline_BehavesCorrectly() { // 模拟HttpClient的响应 var mockHandler = new Mock<HttpMessageHandler>(); mockHandler.Protected() .Setup<Task<HttpResponseMessage>>( "SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) // 可以模拟多次失败再成功,用来验证重试逻辑 .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{\"data\": 123}") }); var httpClient = new HttpClient(mockHandler.Object); var sut = new MyClient(httpClient); // 构建和AddStandardResilienceHandler等价的弹性管道 var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>() .AddRetry(new HttpRetryStrategyOptions()) .AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions()) .AddTimeout(new HttpTimeoutStrategyOptions()) .AddRateLimiter(new HttpRateLimiterStrategyOptions()) .AddFallback(new HttpFallbackStrategyOptions()) .Build(); // 通过管道执行Http请求(如果MyClient的GetData内部调用了HttpClient,也可以直接包裹该方法) var response = await pipeline.ExecuteAsync(async token => { return await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/data"), token); }); // 解析响应并断言 var data = await response.Content.ReadFromJsonAsync<DataModel>(); Assert.Equal(123, data.Data); // 验证重试逻辑:如果模拟了一次失败一次成功,这里断言SendAsync被调用2次 mockHandler.Protected().Verify( "SendAsync", Times.Exactly(2), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()); }
注意事项
- 方案一更推荐,因为能保证测试和生产环境的策略配置完全一致,避免出现测试通过但生产出问题的情况
- 如果要验证弹性策略的具体行为(比如重试次数、断路器触发条件),可以通过模拟Http响应的失败次数,结合Mock的调用次数断言来实现
内容的提问来源于stack exchange,提问作者phil
相关产品推荐
相关产品推荐

