我在尝试针对单例运行单元测试时遇到了一种情况。 我使用单例来管理单个 HttpClient,我的应用程序可以使用它来发出 Http 请求。我知道还有其他方法可以做到这一点,但这是我目前正在使用的方法。
在正常操作期间,这种方法没有问题,因为 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 //This is provided for testing, so I can mock the HttpMessageHandler
{
_httpClient = value;
}
}
private MySingleton()
{
SocketsHttpHandler
handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(10)
};
_httpClient = new HttpClient(handler);
}
internal async Task<bool> SendRequest()
{
//Uses the HttpClient to send a request.
return true;
}
}
有了这个,每当我的应用程序需要发送 Http 请求时,它就可以调用
await MySingleton.Instance.Send()
。这在正常操作中工作得很好。
当我运行单元测试时,问题就出现了,因为它们操作 HttpClient 并并行运行。目前我正在强制测试同步运行,但这不是一个理想的解决方案。 这是我的测试的一个想法。
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.HttpStatus.Created);
MySingleton.Instance.HttpClient = new HttpClient(httpMessageHandler);
bool
result = await MySingleton.Instance.SendRequest();
Assert.IsTrue(result);
}
}
MockHttpMessageHandler
对象只是我编写的一个扩展了HttpMessageHandler
类的类,因此我可以操纵来自HttpClient的响应。
正如您从测试代码中看到的,我将单例中的 HttpClient 替换为将返回我想要的数据的 HttpClient。 当测试并行运行时,这会产生导致测试失败的副作用。
我想知道是否有一种方法可以同步对单例实例的访问,以便它一次只能被一个线程使用。基本上,一个线程会获取单例,将其用于所需的用途,然后将其释放回以供另一个线程使用。
也许我的做法是完全错误的,在这种情况下,我愿意接受建议。虽然我真的很想知道是否可以做我想做的事情。我试图用谷歌搜索答案,但只想到了同步实例的创建(Lazy 已经处理了)。我什至尝试过人工智能,尽管那没有任何帮助。
您可以非常轻松地同步访问。一种方法是用执行同步的方法替换
HttpClient
属性。就像下面这样。
// this code replaces the HttpClient property
private HttpClient _httpClient;
private static object lockObj = new object();
// Attempts to acquire the object.
// If it can't, it returns null.
public static HttpClient TryGet()
{
if (Monitor.TryEnter(lockObj))
return HttpClient;
return false;
}
// Attempts to acquire the object.
// Blocks until it can.
public static HttpClient Get()
{
Monitor.Enter(lockObj);
return HttpClient;
}
// Releases the previously-acquired object
public static Release()
{
Monitor.Exit(lockObj);
}
要使用它,您可以调用
Get
或 TryGet
来获取对 HttpClient
的独占访问权限。完成后,请致电 Release
。比如:
var client = MySingleton.Get();
try
{
// do whatever you need to do with client
}
finally
{
MySingleton.Release();
}
适用标准注意事项。如果获取资源的线程终止而没有释放它,则可能会遇到问题。老实说,我不知道
Monitor
如何处理放弃。