A class contains an attribute that should be created only one time. The creation process is via a Func<T>
which is pass in argument. This is a part of a caching scenario.
The test take care that no matter how many threads try to access the element, the creation occurs only once.
The mechanism of the unit test is to launch a great number of threads around the accessor, and count how many times the creation function is called.
This is not deterministic at all, nothing guaranteed that this is effectively testing a multithread access. Maybe there will be only one thread at a time that will hit the lock. (In reality, getFunctionExecuteCount
is between 7 and 9 if the lock
is not there... On my machine, nothing guaranteed that on the CI server it's going to be the same)
How the unit test can be rewritten in a deterministic way? How to be sure that the lock
is triggered multiple times by multiple thread?
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);
}
}
}
The worst scenario is if someone break the lock
code, and the testing server had some lag. This test shouldn't pass:
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);
}
}
}