一个类包含一个属性,应该只创建一次。创建过程是通过传入参数的 Func<T>
来实现的。这是缓存场景的一部分。
测试确保无论有多少线程尝试访问该元素,创建只会发生一次。
单元测试的机制是在访问器周围启动大量线程,并计算创建函数被调用的次数。
这一点根本不确定,没有任何保证这实际上测试了多线程访问。也许一次只有一个线程会命中锁定。(在现实中,如果没有lock
,getFunctionExecuteCount
介于7到9之间... 在我的机器上,不能保证在CI服务器上它会是相同的)
如何以确定性的方式重写单元测试?如何确保lock
被多个线程多次触发?
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Example.Test
{
public class MyObject<T> where T : class
{
private readonly object _lock = new object();
private T _value = null;
public T Get(Func<T> creator)
{
if (_value == null)
{
lock (_lock)
{
if (_value == null)
{
_value = creator();
}
}
}
return _value;
}
}
[TestClass]
public class UnitTest1
{
[TestMethod]
public void MultipleParallelGetShouldLaunchGetFunctionOnlyOnce()
{
int getFunctionExecuteCount = 0;
var cache = new MyObject<string>();
Func<string> creator = () =>
{
Interlocked.Increment(ref getFunctionExecuteCount);
return "Hello World!";
};
// Launch a very big number of thread to be sure
Parallel.ForEach(Enumerable.Range(0, 100), _ =>
{
cache.Get(creator);
});
Assert.AreEqual(1, getFunctionExecuteCount);
}
}
}
最糟糕的情况是如果有人破解了“锁定”代码,并且测试服务器出现了一些延迟。这个测试不应该通过:
using NUnit.Framework;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Example.Test
{
public class MyObject<T> where T : class
{
private readonly object _lock = new object();
private T _value = null;
public T Get(Func<T> creator)
{
if (_value == null)
{
// oups, some intern broke the code
//lock (_lock)
{
if (_value == null)
{
_value = creator();
}
}
}
return _value;
}
}
[TestFixture]
public class UnitTest1
{
[Test]
public void MultipleParallelGetShouldLaunchGetFunctionOnlyOnce()
{
int getFunctionExecuteCount = 0;
var cache = new MyObject<string>();
Func<string> creator = () =>
{
Interlocked.Increment(ref getFunctionExecuteCount);
return "Hello World!";
};
Parallel.ForEach(Enumerable.Range(0, 2), threadIndex =>
{
// testing server has lag
Thread.Sleep(threadIndex * 1000);
cache.Get(creator);
});
// 1 test passed :'(
Assert.AreEqual(1, getFunctionExecuteCount);
}
}
}
Lazy<T>
类呢?它似乎可以完全达到你想要实现的目标。链接 - MaartenLazy
可以解决这个小例子中的问题。我的真实情况更复杂,涉及到更高级的情况,我不能使用Lazy
类。 - Cyril Gandon