如何在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的管理与业务逻辑,通过依赖注入实现灵活替换:
- 定义业务接口:
public interface IRequestSender { Task<bool> SendRequest(); }
- 实现业务类:
internal sealed class RequestSender : IRequestSender { private readonly HttpClient _httpClient; // 构造函数注入HttpClient public RequestSender(HttpClient httpClient) { _httpClient = httpClient; } public async Task<bool> SendRequest() { // 使用HttpClient发送请求的逻辑 return true; } }
- 生产环境注册服务:
services.AddHttpClient<IRequestSender, RequestSender>() .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(10) });
- 测试代码直接注入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
相关产品推荐
相关产品推荐

