You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 04:55:29