.NET中如何为Typed Client编写集成测试
如何为依赖HttpClient的Typed Client编写集成测试?
我有一个.NET 7控制台应用,用于从API获取数据,使用了自定义的Typed Client,其服务注册代码如下:
services.AddSingleton<IMyTypedClient, MyTypedClient>(); services.AddHttpClient<IMyTypedClient, MyTypedClient>(client => { client.BaseAddress = new Uri("http://www.randomnumberapi.com/api/v1.0/"); }) .SetHandlerLifetime(TimeSpan.FromMinutes(5)) .AddPolicyHandler(GetRetryPolicy()); static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound) .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); }
对应的Typed Client实现代码如下:
public class MyTypedClient : IMyTypedClient { private readonly HttpClient _httpClient; public MyTypedClient(HttpClient httpClient) { _httpClient = httpClient; } public void GetRandomNumber() { var response = _httpClient.GetAsync("/random?min=1&max=10&count=5").Result; if (response.StatusCode != HttpStatusCode.OK) { throw new Exception("Failed to get random number"); } } }
测试实现方案
集成测试Typed Client的核心是模拟HTTP响应,避免依赖真实外部API,同时验证请求逻辑、重试策略等是否符合预期。以下是两种常用的实现方式:
方式一:自定义HttpMessageHandler模拟基础请求逻辑
通过继承HttpMessageHandler重写SendAsync方法,预设HTTP响应,同时可以验证请求的URL、方法是否符合预期。
测试代码示例
// 自定义模拟HttpHandler public class MockHttpMessageHandler : HttpMessageHandler { private readonly HttpResponseMessage _response; public MockHttpMessageHandler(HttpResponseMessage response) { _response = response; } protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 验证请求参数是否正确 Assert.Equal(HttpMethod.Get, request.Method); Assert.Equal("/random?min=1&max=10&count=5", request.RequestUri?.PathAndQuery); return Task.FromResult(_response); } } // 测试类 public class MyTypedClientTests { [Fact] public void GetRandomNumber_ResponseOk_DoesNotThrow() { // 准备模拟成功响应 var mockResponse = new HttpResponseMessage(HttpStatusCode.OK); var handler = new MockHttpMessageHandler(mockResponse); var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://www.randomnumberapi.com/api/v1.0/") }; // 实例化Typed Client var typedClient = new MyTypedClient(httpClient); // 执行测试,验证无异常抛出 var exception = Record.Exception(() => typedClient.GetRandomNumber()); Assert.Null(exception); } [Fact] public void GetRandomNumber_ResponseNotOk_ThrowsException() { // 准备模拟失败响应 var mockResponse = new HttpResponseMessage(HttpStatusCode.BadRequest); var handler = new MockHttpMessageHandler(mockResponse); var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://www.randomnumberapi.com/api/v1.0/") }; var typedClient = new MyTypedClient(httpClient); // 验证抛出指定异常 var exception = Assert.Throws<Exception>(() => typedClient.GetRandomNumber()); Assert.Equal("Failed to get random number", exception.Message); } }
方式二:结合HttpClientFactory验证重试策略
如果需要验证Polly重试策略的执行逻辑,可以在测试中重新构建服务容器,注入模拟Handler并复用原有重试配置。
测试代码示例
public class MyTypedClientRetryTests { [Fact] public void GetRandomNumber_TransientError_Retries6Times() { int requestCount = 0; // 自定义Handler统计请求次数 var mockHandler = new MockHttpMessageHandler(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)) { SendAsyncFunc = (request, token) => { requestCount++; return Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)); } }; // 构建测试用服务容器 var services = new ServiceCollection(); services.AddHttpClient<IMyTypedClient, MyTypedClient>(client => { client.BaseAddress = new Uri("http://www.randomnumberapi.com/api/v1.0/"); }) .SetHandlerLifetime(TimeSpan.FromMinutes(5)) .AddPolicyHandler(GetRetryPolicy()) // 复用原有重试策略 .ConfigurePrimaryHttpMessageHandler(() => mockHandler); var serviceProvider = services.BuildServiceProvider(); var typedClient = serviceProvider.GetRequiredService<IMyTypedClient>(); // 执行测试,验证最终抛出异常 Assert.Throws<Exception>(() => typedClient.GetRandomNumber()); // 验证请求次数:1次初始请求 + 6次重试 = 7次 Assert.Equal(7, requestCount); } // 复用原有重试策略方法 static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .OrResult(msg => msg.StatusCode == HttpStatusCode.NotFound) .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); } } // 扩展MockHandler支持自定义请求逻辑 public class MockHttpMessageHandler : HttpMessageHandler { public Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> SendAsyncFunc { get; set; } private readonly HttpResponseMessage _defaultResponse; public MockHttpMessageHandler(HttpResponseMessage defaultResponse) { _defaultResponse = defaultResponse; SendAsyncFunc = (req, token) => Task.FromResult(_defaultResponse); } protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return SendAsyncFunc(request, cancellationToken); } }
优化建议
原Typed Client中的GetRandomNumber使用.Result阻塞异步调用,建议改为异步方法(public async Task GetRandomNumberAsync()),避免潜在死锁问题,同时符合.NET异步最佳实践:
public async Task GetRandomNumberAsync() { var response = await _httpClient.GetAsync("/random?min=1&max=10&count=5"); if (!response.IsSuccessStatusCode) { throw new Exception("Failed to get random number"); } }
内容的提问来源于stack exchange,提问作者wetfield
相关产品推荐
相关产品推荐

