Una classe contiene un attributo che deve essere creato una sola volta. Il processo di creazione è tramite un Func<T>
che è argomento di passaggio. Questa è una parte di uno scenario di memorizzazione nella cache.Come testare il codice di test che dovrebbe essere eseguito una sola volta in uno scenario MultiThread?
Il test fa in modo che, indipendentemente da quanti thread tentano di accedere all'elemento, la creazione si verifica una sola volta.
Il meccanismo del test dell'unità è di avviare un numero elevato di thread attorno all'accessor e contare quante volte viene chiamata la funzione di creazione.
Questo non è affatto deterministico, non è garantito che questo stia effettivamente testando un accesso multithread. Forse ci sarà solo un thread alla volta che colpirà il blocco. (In realtà, getFunctionExecuteCount
è tra 7 e 9 se il lock
non c'è ... Sulla mia macchina, nulla è garantito che sul server CI sarà lo stesso)
Come il test dell'unità può essere riscritto in un modo deterministico? Come essere sicuri che lo lock
venga attivato più volte da più 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);
}
}
}
Lo scenario peggiore è se qualcuno rompe il codice lock
, e il server di prova ha avuto un certo ritardo. Questo test non deve passare:
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);
}
}
}
Se si rende il creatore "lento" (lascia il thread inattivo per un po ') si aumenta la possibilità che altri thread colpiscano il blocco e si debba attendere che il thread lento lasci il blocco. –
Stai provando a testare la sicurezza del thread unitario? Mi sembra strano. Mi sembra che tu testare le funzionalità della classe che esponi, in cui il problema della sicurezza dei thread sarebbe più un argomento per la revisione del codice di questo codice. –
Se qualcuno arriva ed elimina la parte 'lock' del codice, e la revisione del codice non la cattura, non voglio che il test dell'unità lo catturi. –