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

如何在C#中同步访问单例以适配并行单元测试?

单例HttpClient并行单元测试问题及解决方案

在应用中通过单例管理唯一的HttpClient,正常运行时无异常,但并行执行单元测试时,多个测试会替换单例中的HttpClient来模拟响应,导致测试相互干扰失败,目前只能强制同步运行测试,以下是针对性的解决方案:

单例类核心代码

internal sealed class MySingleton
{
    private static readonly Lazy<MySingleton> _instance = new Lazy<MySingleton>(() => new MySingleton());

    public static MySingleton Instance => _instance.Value;

    private HttpClient _httpClient;

    internal HttpClient HttpClient
    {
        get => _httpClient;
        set // 供测试使用,用于Mock HttpMessageHandler
        {
            _httpClient = value;
        }
    }

    private MySingleton()
    {
        SocketsHttpHandler handler = new SocketsHttpHandler
        {
            PooledConnectionLifetime = TimeSpan.FromMinutes(10)
        };
        _httpClient = new HttpClient(handler);
    }

    internal async Task<bool> SendRequest()
    {
        // 使用HttpClient发送请求
        return true;
    }
}

测试示例代码

public class Tests
{
    [Fact]
    public async Task This_Is_A_Test()
    {
        string jsonText = """
        {
            "id" : 1,
            "name" : "testing",
            "value" : 23
        }
        """;
        HttpMessageHandler httpMessageHandler = new MockHttpMessageHandler(jsonText, System.Net.HttpStatusCode.Created);
        MySingleton.Instance.HttpClient = new HttpClient(httpMessageHandler);

        bool result = await MySingleton.Instance.SendRequest();
        Assert.IsTrue(result);
    }
}

解决方案

方案1:实现线程/异步上下文隔离的单例(推荐)

通过AsyncLocal<T>让每个测试的异步上下文拥有独立的单例实例,彻底避免测试间干扰:

internal sealed class MySingleton
{
    // 生产环境全局唯一实例
    private static readonly Lazy<MySingleton> _globalInstance = new Lazy<MySingleton>(() => new MySingleton());
    // 测试环境用于线程隔离的实例容器
    private static readonly AsyncLocal<MySingleton> _testInstance = new AsyncLocal<MySingleton>();

    public static MySingleton Instance
    {
        get
        {
            // 测试优先用线程隔离实例,生产用全局实例
            return _testInstance.Value ?? _globalInstance.Value;
        }
        // 供测试设置独立实例
        internal set => _testInstance.Value = value;
    }

    private HttpClient _httpClient;

    internal HttpClient HttpClient
    {
        get => _httpClient;
        set => _httpClient = value;
    }

    private MySingleton()
    {
        var handler = new SocketsHttpHandler
        {
            PooledConnectionLifetime = TimeSpan.FromMinutes(10)
        };
        _httpClient = new HttpClient(handler);
    }

    internal async Task<bool> SendRequest()
    {
        // 使用HttpClient发送请求的逻辑
        return true;
    }
}

修改测试代码,为当前测试创建独立实例:

public class Tests
{
    [Fact]
    public async Task This_Is_A_Test()
    {
        string jsonText = """
        {
            "id" : 1,
            "name" : "testing",
            "value" : 23
        }
        """;
        var httpMessageHandler = new MockHttpMessageHandler(jsonText, System.Net.HttpStatusCode.Created);
        
        // 创建当前测试专属的单例实例
        var testInstance = new MySingleton();
        testInstance.HttpClient = new HttpClient(httpMessageHandler);
        MySingleton.Instance = testInstance;

        bool result = await MySingleton.Instance.SendRequest();
        Assert.IsTrue(result);
    }
}

方案2:加锁控制HttpClient的替换(临时应急)

如果不想修改单例结构,可通过全局锁确保同一时间只有一个测试修改和使用单例的HttpClient:

public class Tests
{
    private static readonly object _lockObj = new object();

    [Fact]
    public async Task This_Is_A_Test()
    {
        string jsonText = """
        {
            "id" : 1,
            "name" : "testing",
            "value" : 23
        }
        """;
        var httpMessageHandler = new MockHttpMessageHandler(jsonText, System.Net.HttpStatusCode.Created);
        
        lock(_lockObj)
        {
            MySingleton.Instance.HttpClient = new HttpClient(httpMessageHandler);
            bool result = await MySingleton.Instance.SendRequest();
            Assert.IsTrue(result);
            // 可选:测试后恢复原HttpClient,避免影响其他测试
            // MySingleton.Instance.HttpClient = new HttpClient(new SocketsHttpHandler{ PooledConnectionLifetime = TimeSpan.FromMinutes(10) });
        }
    }
}

注:此方案本质是串行执行测试逻辑,性能提升有限,仅作临时过渡使用。

方案3:依赖注入重构(最佳实践)

从根源解耦HttpClient的管理与业务逻辑,通过依赖注入实现灵活替换:

  1. 定义业务接口:
public interface IRequestSender
{
    Task<bool> SendRequest();
}
  1. 实现业务类:
internal sealed class RequestSender : IRequestSender
{
    private readonly HttpClient _httpClient;

    // 构造函数注入HttpClient
    public RequestSender(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<bool> SendRequest()
    {
        // 使用HttpClient发送请求的逻辑
        return true;
    }
}
  1. 生产环境注册服务:
services.AddHttpClient<IRequestSender, RequestSender>()
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(10)
});
  1. 测试代码直接注入Mock实例:
public class Tests
{
    [Fact]
    public async Task This_Is_A_Test()
    {
        string jsonText = """
        {
            "id" : 1,
            "name" : "testing",
            "value" : 23
        }
        """;
        var httpMessageHandler = new MockHttpMessageHandler(jsonText, System.Net.HttpStatusCode.Created);
        var httpClient = new HttpClient(httpMessageHandler);
        
        // 直接创建测试用实例,注入Mock的HttpClient
        var sender = new RequestSender(httpClient);

        bool result = await sender.SendRequest();
        Assert.IsTrue(result);
    }
}

此方案符合SOLID原则,不仅解决测试问题,还提升了代码的扩展性与可维护性。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 20:55:19