Je travaille avec une nouvelle base de code qui fait un usage intensif de async/wait. La plupart des membres de mon équipe sont également relativement nouveaux dans async/wait. Nous avons généralement tendance à nous en tenir à Meilleures pratiques spécifiées par Microsoft , mais nous avons généralement besoin de notre contexte pour passer par l'appel asynchrone et travaillons avec des bibliothèques qui ne ConfigureAwait(false)
.
Combinez toutes ces choses et nous rencontrons des blocages asynchrones décrits dans l'article ... hebdomadaire. Ils n'apparaissent pas lors des tests unitaires, car nos sources de données simulées (généralement via Task.FromResult
) ne suffisent pas à déclencher le blocage. Ainsi, lors des tests d'exécution ou d'intégration, certains appels de service sortent pour le déjeuner et ne reviennent jamais. Cela tue les serveurs et fait généralement un gâchis.
Le problème est que la recherche de l'endroit où l'erreur a été commise (généralement pas tout à fait asynchrone) implique généralement une inspection manuelle du code, ce qui prend du temps et n'est pas automatisable.
Quelle est la meilleure façon de diagnostiquer la cause de l'impasse?
Ok - Je ne sais pas si les éléments suivants vous seront utiles, car j'ai fait certaines hypothèses dans le développement d'une solution qui peut ou non être vraie dans votre cas. Peut-être que ma "solution" est trop théorique et ne fonctionne que pour des exemples artificiels - je n'ai pas fait de test au-delà des choses ci-dessous.
. après avoir été posté, j'ai commencé à jouer avec le problème).
Mais assez dit: Disons que nous avons un service de données simple qui peut être utilisé pour récupérer un entier:
public interface IDataService
{
Task<int> LoadMagicInteger();
}
Une implémentation simple utilise du code asynchrone:
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;
}
}
Maintenant, un problème se pose, si nous utilisons le code "incorrectement" comme illustré par cette classe. Foo
accède incorrectement Task.Result
au lieu de await
le résultat comme Bar
fait:
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;
}
}
Ce dont nous (vous) avons maintenant besoin, c'est d'un moyen d'écrire un test qui réussit lors de l'appel de Bar
mais échoue lors de l'appel de Foo
(du moins si j'ai bien compris la question ;-)).
Je vais laisser le code parler; voici ce que j'ai trouvé (en utilisant des tests Visual Studio, mais cela devrait aussi fonctionner en utilisant NUnit):
DataServiceMock
utilise TaskCompletionSource<T>
. Cela nous permet de définir le résultat à un point défini dans le test, ce qui conduit au test suivant. Notez que nous utilisons un délégué pour renvoyer le TaskCompletionSource dans le test. Vous pouvez également mettre cela dans la méthode Initialize des propriétés test et use.
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;
Ce qui se passe ici, c'est que nous vérifions d'abord que nous pouvons quitter la méthode sans bloquer (cela ne fonctionnerait pas si quelqu'un accédait à Task.Result
- dans ce cas, nous aurions un délai d'attente car le résultat de la tâche n'est rendu disponible qu'après le retour de la méthode).
Ensuite, nous définissons le résultat (maintenant la méthode peut s'exécuter) et nous vérifions le résultat (dans un test unitaire, nous pouvons accéder à Task.Result comme nous le voulons réellement le blocage se produit).
Classe de test complète - BarTest
réussit et FooTest
échoue comme souhaité.
[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;
}
}
Et une petite classe d'aide pour tester les blocages/délais d'attente:
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);
}
}