Ok - Non sono sicuro che quanto segue ti sarà di alcun aiuto, perché ho fatto alcune ipotesi nello sviluppo di una soluzione che può o non può essere vera nel tuo caso. Forse la mia "soluzione" è troppo teorica e funziona solo per esempi artificiali - Non ho fatto nessun test oltre a quello qui sotto.
Inoltre, vedrei più una soluzione alternativa a una soluzione reale, ma considerando la mancanza di risposte penso che potrebbe essere ancora meglio di niente (ho continuato a guardare la tua domanda in attesa di una soluzione, ma non vedendone una pubblicata ho iniziato a giocare in giro con il problema).
Ma abbastanza detto: supponiamo di avere un semplice servizio dati che può essere utilizzato per recuperare un intero:
public interface IDataService
{
Task<int> LoadMagicInteger();
}
Una semplice implementazione utilizza un codice asincrono:
public sealed class CustomDataService
: IDataService
{
public async Task<int> LoadMagicInteger()
{
Console.WriteLine("LoadMagicInteger - 1");
await Task.Delay(100);
Console.WriteLine("LoadMagicInteger - 2");
var result = 42;
Console.WriteLine("LoadMagicInteger - 3");
await Task.Delay(100);
Console.WriteLine("LoadMagicInteger - 4");
return result;
}
}
Ora, si presenta un problema, se stiamo usando il codice "erroneamente" come illustrato da questa classe. Foo
accede in modo errato a Task.Result
invece di await
ing il risultato come Bar
fa:
public sealed class ClassToTest
{
private readonly IDataService _dataService;
public ClassToTest(IDataService dataService)
{
this._dataService = dataService;
}
public async Task<int> Foo()
{
var result = this._dataService.LoadMagicInteger().Result;
return result;
}
public async Task<int> Bar()
{
var result = await this._dataService.LoadMagicInteger();
return result;
}
}
Ciò di cui ora abbiamo bisogno è un modo per scrivere un test che ha successo quando si chiama Bar
ma fallisce quando si chiama Foo
(almeno se ho capito correttamente la domanda ;-)).
Lascerò che il codice parli; ecco cosa mi è venuto in mente (usando i test di Visual Studio, ma dovrebbe funzionare anche con NUnit):
DataServiceMock
utilizza TaskCompletionSource<T>
. Questo ci consente di impostare il risultato in un punto definito nell'esecuzione del test che conduce al test seguente. Si noti che stiamo utilizzando un delegato per ritrasferire TaskCompletionSource nel test. Puoi anche inserire questo nel metodo Initialize del test e utilizzare le proprietà.
TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;
Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());
tcs.TrySetResult(42);
var result = task.Result;
Assert.AreEqual(42, result);
this._end = true;
Quello che sta succedendo qui è che prima verifichiamo che possiamo lasciare il metodo senza bloccare (questo non funzionerebbe se qualcuno accedesse a Task.Result
- in questo caso verrebbe eseguito un timeout in quanto il risultato dell'attività non è reso disponibile fino a dopo il ritorno del metodo).
Quindi, impostiamo il risultato (ora il metodo può essere eseguito) e verifichiamo il risultato (all'interno di un test unitario possiamo accedere a Task.Result poiché in realtà vogliamo il blocco che si verifica).
Completa la classe di test - BarTest
ha successo e FooTest
fallisce come desiderato.
[TestClass]
public class UnitTest1
{
private DataServiceMock _dataService;
private ClassToTest _instance;
private bool _end;
[TestInitialize]
public void Initialize()
{
this._dataService = new DataServiceMock();
this._instance = new ClassToTest(this._dataService);
this._end = false;
}
[TestCleanup]
public void Cleanup()
{
Assert.IsTrue(this._end);
}
[TestMethod]
public void FooTest()
{
TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;
Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());
tcs.TrySetResult(42);
var result = task.Result;
Assert.AreEqual(42, result);
this._end = true;
}
[TestMethod]
public void BarTest()
{
TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;
Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Bar());
tcs.TrySetResult(42);
var result = task.Result;
Assert.AreEqual(42, result);
this._end = true;
}
}
E una piccola classe di supporto per testare deadlock / timeout:
public static class TaskTestHelper
{
public static void AssertDoesNotBlock(Action action, int timeout = 1000)
{
var timeoutTask = Task.Delay(timeout);
var task = Task.Factory.StartNew(action);
Task.WaitAny(timeoutTask, task);
Assert.IsTrue(task.IsCompleted);
}
}