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

如何在单元测试中通过依赖注入模拟带Polly策略的静态HttpClient

如何单元测试带有Polly策略的静态HttpClient单例类

首先,咱们先梳理下现有代码里的几个影响测试的小问题:

  • 静态的_httpClient字段在构造函数里被重新赋值,但单例的Instance属性并没有使用这个构造函数初始化实例,导致注入的HttpClient根本不会被用到
  • Policies是直接实例化的,没有抽象层,没法在测试中替换成模拟对象

要实现可测试的依赖注入,咱们分两步走:先重构Client类使其支持依赖注入,再编写单元测试时模拟所需的依赖。

第一步:重构Client类,提升可测试性

1. 抽象Polly策略

先给Policies定义一个接口,这样我们可以在测试中轻松替换成模拟实现:

public interface IPolicies
{
    // 这里定义你的Polly策略方法,比如重试、熔断等
    IAsyncPolicy<HttpResponseMessage> GetRetryPolicy();
}

public class Policies : IPolicies
{
    public IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
    {
        // 原有的Polly策略实现示例
        return Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
                     .RetryAsync(3);
    }
}

2. 调整单例Client类,支持依赖注入

把静态HttpClient改成构造注入,同时给单例实例提供初始化入口,允许传入依赖(用于测试或DI初始化):

public class Client 
{ 
    private readonly HttpClient _httpClient;
    private readonly IPolicies _policies;

    // 私有构造函数,配合Instance属性使用
    private Client(HttpClient httpClient, IPolicies policies) 
    { 
        _httpClient = httpClient;
        _policies = policies;
    } 

    // 静态实例
    private static Client _instance;
    private static readonly object _padlock = new object();

    // 提供初始化方法,允许传入依赖
    public static void Initialize(HttpClient httpClient, IPolicies policies)
    {
        lock (_padlock)
        {
            _instance = new Client(httpClient, policies);
        }
    }

    public static Client Instance 
    { 
        get 
        { 
            lock (_padlock) 
            { 
                if (_instance == null)
                {
                    // 生产环境下的默认初始化
                    _instance = new Client(new HttpClient(), new Policies());
                }
                return _instance;
            }
        }
    }

    // 示例业务方法:使用Polly策略发起请求
    public async Task<HttpResponseMessage> GetAsync(string url)
    {
        var policy = _policies.GetRetryPolicy();
        return await policy.ExecuteAsync(() => _httpClient.GetAsync(url));
    }
}

第二步:编写单元测试,模拟依赖

我们可以用Moq框架来模拟HttpClient和IPolicies,然后初始化Client的单例实例进行测试:

1. 模拟HttpClient

通过模拟HttpMessageHandler来控制HttpClient的响应:

// 创建模拟的HttpMessageHandler
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("Test Response")
           });

// 使用模拟handler创建HttpClient
var mockHttpClient = new HttpClient(mockHandler.Object);

2. 模拟Polly策略

模拟IPolicies,让它返回一个无操作的策略(避免测试时触发真实的重试逻辑):

var mockPolicies = new Mock<IPolicies>();
mockPolicies.Setup(p => p.GetRetryPolicy())
            .Returns(Policy.NoOpAsync<HttpResponseMessage>());

3. 初始化Client并执行完整测试

[TestClass]
public class ClientTests
{
    [TestMethod]
    public async Task GetAsync_ShouldReturnMockResponse()
    {
        // 准备模拟依赖
        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("Test Response")
                   });
        var mockHttpClient = new HttpClient(mockHandler.Object);

        var mockPolicies = new Mock<IPolicies>();
        mockPolicies.Setup(p => p.GetRetryPolicy())
                    .Returns(Policy.NoOpAsync<HttpResponseMessage>());

        // 初始化Client单例
        Client.Initialize(mockHttpClient, mockPolicies.Object);

        // 执行测试方法
        var response = await Client.Instance.GetAsync("https://example.com");

        // 断言结果
        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual("Test Response", await response.Content.ReadAsStringAsync());

        // 验证HttpClient的SendAsync被调用一次
        mockHandler.Protected()
                   .Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
    }
}

额外建议:尽量避免手动单例实现

手动实现的单例锁虽然能工作,但在依赖注入场景下,更推荐让DI容器(比如ASP.NET Core的IServiceCollection)来管理单例:

// 在Program.cs中注册服务
services.AddSingleton<IPolicies, Policies>();
services.AddHttpClient<Client>();

// 如果需要保留静态Instance属性,可以从DI容器中获取实例赋值
var serviceProvider = services.BuildServiceProvider();
var client = serviceProvider.GetRequiredService<Client>();
Client.Initialize(client._httpClient, client._policies);

这样在测试时,你可以直接向DI容器注入模拟的IPolicies和HttpClient,不需要手动处理单例的初始化逻辑。

内容的提问来源于stack exchange,提问作者Kal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 11:59:45