Share via


Cet article a fait l'objet d'une traduction automatique.

Programmation asynchrone

Code de test d'unité asynchrone

Stephen Cleary

Télécharger l'exemple de code

Test unitaire est des pierres angulaires du développement moderne. Les avantages des tests d'unités pour un projet sont assez bien connues : Test unitaire diminue le nombre de bogues, réduit le temps au marché et décourage la conception trop couplée. Ceux sont des avantages tout gentils, mais il y a d'autres avantages plus directement les développeurs. Quand j'écris des tests unitaires, je peux avoir beaucoup plus de confiance dans le code. Il est plus facile d'ajouter des fonctionnalités ou corriger des bogues dans le code testé, car l'unité teste Loi comme filet de sécurité, tandis que le code change.

Écriture de tests unitaires pour le code asynchrone apporte quelques défis uniques. En outre, l'état actuel d'async soutenir en test unitaire et se moquant des cadres varie et évolue encore. Cet article examinera MSTest, NUnit et xUnit, mais les principes généraux s'appliquent à n'importe quel framework de tests unitaires. La plupart des exemples dans cet article utilise la syntaxe MSTest, mais je vais souligner toutes les différences dans le comportement sur la route. Le téléchargement de code contient des exemples pour tous les trois cadres.

Avant de plonger dans les détails, je vais examiner brièvement un modèle conceptuel de la façon dont l'async et attendre les travaux de mots-clés.

Async et attendent en bref

Le mot-clé async fait deux choses : elle permet le mot-clé await dans cette méthode, et il transforme la méthode en une machine à États (semblable à comment le mot-clé yield transforme des blocs d'itérateur en machines d'État). Méthodes Async doivent retourner Task ou Task < T > Lorsque cela est possible. Il n'est permis pour une méthode async retourne void, mais il n'est pas recommandé car il est très difficile de consommer (ou test) une méthode void async.

L'instance de tâche retournée par une méthode async est géré par l'ordinateur d'État. La machine d'État va créer l'instance de tâche pour revenir et terminera plus tard cette tâche.

Une méthode asynchrone commence à exécuter de manière synchrone. C'est seulement lorsque la méthode async atteint un opérateur await que la méthode peut devenir asynchrone. L'opérateur await prend un seul argument, une « await » telles qu'une instance de tâche. Tout d'abord, l'opérateur await vérifiera l'await si il a déjà terminé ; Si elle a, la méthode continue (de façon synchrone). Si l'await n'est pas encore terminée, l'opérateur await « pause » de la méthode et reprendre lorsque l'await est terminée. La deuxième action de l'opérateur await est extraire aucun résultat les await, soulevant des exceptions si l'await terminé avec une erreur.

La tâche ou la tâche < T > retourné par l'async méthode représente conceptuellement l'exécution de cette méthode. La tâche va terminer lorsque la méthode est terminée. Si la méthode retourne une valeur, la tâche est terminée avec cette valeur à sa suite. Si la méthode lève une exception (et ne l'attrape), la tâche est terminée avec cette exception.

Il y a deux leçons immédiates à tirer de ce bref aperçu. Tout d'abord, lorsque vous testez les résultats d'une méthode asynchrone, le bit important est la tâche il retourne. La méthode async utilise sa tâche pour achèvement, les résultats et les exceptions. La deuxième leçon est que l'opérateur await a un comportement spécial lorsque son await est déjà terminée. J'aborderai ceci plus tard lors de l'examen des talons asynchrones.

Le mal en passant de Test unitaire

En économie de marché libre, les pertes sont tout aussi importants que les bénéfices ; C' est l'échec des entreprises qui les obligent à produire ce que les gens vont acheter et encourager l'allocation de ressources optimale au sein du système dans son ensemble. De même, les échecs des tests unitaires sont tout aussi importants que leurs succès. Vous devez être sûr que le test unitaire échoue quand il se doit, ou son succès ne veut rien dire.

Un test unitaire qui est censé ne pas (à tort) réussira quand il teste la mauvaise chose. C'est pourquoi le test driven development (TDD) fait une utilisation intensive de la boucle rouge/vert/refactor : la partie « rouge » de la boucle assure que le test unitaire échoue lorsque le code est incorrect. Au code tout d'abord, les test que vous savez pour être fausses sons ridicules, mais c'est en fait très important car vous devez être sûr que les tests échouera lorsqu'ils ont besoin pour. La partie rouge de la boucle de TDD teste en fait les tests.

Dans ce contexte, envisager la méthode asynchrone suivante pour tester :

public sealed class SystemUnderTest
{
  public static async Task SimpleAsync()
  {
    await Task.Delay(10);
  }
}

Les nouveaux arrivants aux tests unitaires d'async fera souvent un test comme ça comme une première tentative :

// Warning: bad code!
[TestMethod]
public void IncorrectlyPassingTest()
{
  SystemUnderTest.SimpleAsync();
}

Malheureusement, ce test unitaire ne tester réellement correctement la méthode asynchrone. Si je modifie le code sous test échoue, le test unitaire passera encore :

public sealed class SystemUnderTest
{
  public static async Task SimpleAsync()
  {
    await Task.Delay(10);
    throw new Exception("Should fail.");
  }
}

Ceci illustre la première leçon du modèle conceptuel async/vous attendent : Pour tester le comportement de la méthode asynchrone, vous devez respecter la tâche qu'il retourne. La meilleure façon de le faire est d'attendre la tâche retournée par la méthode testée. Cet exemple illustre également l'avantage de la rouge/vert/refactor test cycle de développement ; vous devez vérifier que les tests échoue lorsque le code sous test échoue.

Infrastructures de tests unitaires plus modernes prennent en charge les tests d'unité asynchrone retourne des tâches. La méthode de IncorrectlyPassingTest provoquera du compilateur AVERTISSEMENT CS4014, qui recommande l'utilisation vous attendent à consommer la tâche retournée par SimpleAsync. Lorsque la méthode de test unitaire est modifiée pour attendre la tâche, l'approche plus naturelle est de changer la méthode d'essai pour être un méthode spéciale d'async. Cela garantit que la méthode de test échouera (correctement) :

[TestMethod]
public async Task CorrectlyFailingTest()
{
  await SystemUnderTest.FailAsync();
}

Évitant les Tests unitaires Async vide

Les utilisateurs expérimentés d'async savent pour éviter le vide async. J'ai décrit les problèmes avec async Sub dans mon article de mars 2013, « Les meilleures pratiques en programmation asynchrone » (bit.ly/1ulDCiI). Méthodes de test pour le sub unitaire Async ne fournissent pas un moyen facile pour leur infrastructure de test d'unité extraire les résultats du test. Malgré cette difficulté, certaines infrastructures de tests unitaires prennent en charge async Sub unit tests en fournissant leur propre SynchronizationContext dans lequel ses tests unitaires sont exécutés.

Fournissant un SynchronizationContext est quelque peu controversée, car il ne modifie pas l'environnement dans lequel les tests exécutés. En particulier, lorsqu'une méthode async attend une tâche, par défaut, il va reprendre cette méthode async sur le SynchronizationContext actuel. Si la présence ou l'absence d'un SynchronizationContext changera indirectement le comportement du système sous test. Si vous êtes curieux de connaître les détails de SynchronizationContext, voir mon article MSDN Magazine sur le sujet à bit.ly/1hIar1p.

MSTest ne fournit pas un SynchronizationContext. En fait, lorsque MSBuild est découverte de tests dans un projet qui utilise des tests d'unité Sub async, il permet de détecter cette et question AVERTISSEMENT UTA007, notifier l'utilisateur que l'unité de la méthode d'essai doit retourner la tâche au lieu de vide. MSBuild ne sera pas exécuter des tests d'unité Sub async.

NUnit prend en charge les tests unitaires Sub, async, depuis la version 2.6.2. La prochaine mise à jour majeure de NUnit, version 2.9.6, soutient async Sub unit tests, mais les développeurs ont décidé de retirer le support en version 2.9.7. NUnit prévoit seulement un SynchronizationContext async Sub unit tests.

A ce jour, xUnit envisage d'ajouter le support pour les tests d'unité Sub async avec version 2.0.0. À la différence de NUnit, xUnit fournit un SynchronizationContext pour toutes ses méthodes d'essai, même les plus synchrones. Cependant, avec MSTest ne pas soutenir async Sub unit tests et NUnit, qui a infirmé sa décision antérieure et de retrait de soutien, je serais surpris si xUnit également choisit d'abandonner l'async Sub unit test soutien avant que la version 2 est publiée.

L'essentiel est que les tests d'unité Sub async sont compliqués pour cadres à soutenir et n'apporter aucun avantage sur async tests unitaires tâche exige des changements dans l'environnement d'exécution de test. En outre, le soutien aux essais unité Sub async varie de cadres et même des versions de framework. Pour ces raisons, il est préférable d'éviter async Sub unit tests.

Tests unitaires tâche Async

Tests unitaires asynchrones qui retournent la tâche n'en ont pas des problèmes des tests unitaires asynchrones qui retournent void. Tests unitaires asynchrones qui retournent tâche bénéficient d'un soutien large de presque toutes les infrastructures de tests unitaires. MSTest ajouté le support de Visual Studio 2012, NUnit dans les versions 2.6.2 et 2.9.6 et xUnit en version 1.9. Donc, tant que votre infrastructure de test unitaire est inférieure à 3 ans, des tests unitaires asynchrones tâche devraient fonctionner.

Malheureusement, infrastructures de tests unitaires périmées ne comprennent async tâche des tests unitaires. A ce jour, il y a une plate-forme majeure qui ne les supporte pas : Novell. Novell utilise une ancienne version personnalisée de NUnitLite, et il ne supporte actuellement pas async tâche des tests unitaires. J'attends que le support sera ajouté prochainement. En attendant, j'utilise une solution de contournement qui est inefficace, mais fonctionne : Exécuter la logique du test asynchrone sur un thread de pool de thread différent et puis (de façon synchrone) bloquer la méthode de test unitaire jusqu'à la fin de l'essai proprement dit. Le code de la solution de contournement utilise GetAwaiter().GetResult() au lieu d'attendre car l'attente va envelopper toutes les exceptions à l'intérieur d'une AggregateException :

[Test]
public void XamarinExampleTest()
{
  // This workaround is necessary on Xamarin,
  // which doesn't support async unit test methods.
  Task.Run(async () =>
  {
    // Actual test code here.
  }).GetAwaiter().GetResult();
}

Tester les Exceptions

Lors de l'essai, il est naturel de tester le scénario réussi ; par exemple, un utilisateur peut mettre à jour son propre profil. Cependant, tester les exceptions est également très important ; par exemple, un utilisateur ne devrait pas être en mesure de mettre à jour le profil de quelqu'un d'autre. Exceptions font partie d'une surface d'API comme paramètres de méthode sont. Par conséquent, il est important d'avoir des tests unitaires pour le code quand on s'attend à l'échec.

A l'origine, le ExpectedExceptionAttribute a été placé sur une méthode de test unitaire pour indiquer que le test unitaire devait échouer. Cependant, il y avait quelques problèmes avec ExpectedException­attribut. La première était qu'il pouvait seulement s'attendre à un test unitaire à l'échec dans son ensemble ; Il y n'avait aucun moyen d'indiquer que qu'une partie déterminée du test devait échouer. Ce n'est pas un problème avec les tests très simples, mais peut avoir des résultats trompeurs quand les tests s'allongent. Le deuxième problème avec ExpectedExceptionAttribute est qu'il est limité à vérifier le type de l'exception ; Il n'y a aucun moyen de vérifier les autres attributs, tels que des codes d'erreur ou messages.

Pour ces raisons, ces dernières années on observe un changement envers l'usage de quelque chose de plus comme Assert.ThrowsException, qui prend la partie importante du code en tant que délégué et renvoie l'exception a été levée. Cette action a résolu les lacunes du ExpectedExceptionAttribute. L'infrastructure de MSTest Bureau prend en charge seulement ExpectedExceptionAttribute, tandis que le plus récent cadre de MSTest utilisé pour les projets de test Windows Store unité prend en charge uniquement Assert.ThrowsException. xUnit prend en charge seulement Assert.Throws et NUnit prend en charge les deux approches. Figure 1 est un exemple de ces deux types de tests, à l'aide de la syntaxe de MSTest.

Figure 1 tester les Exceptions avec les méthodes d'essai synchrone

// Old style; only works on desktop.
[TestMethod]
[ExpectedException(typeof(Exception))]
public void ExampleExpectedExceptionTest()
{
  SystemUnderTest.Fail();
}
// New style; only works on Windows Store.
[TestMethod]
public void ExampleThrowsExceptionTest()
{
  var ex = Assert.ThrowsException<Exception>(() 
    => { SystemUnderTest.Fail(); });
}

Mais qu'en est-il de code asynchrone ? Unité opérationnelle Async tests fonctionnent parfaitement bien avec ExpectedExceptionAttribute MSTest et NUnit (xUnit ne supporte pas ExpectedExceptionAttribute à tous). Cependant, le support d'une ThrowsException prête à async est moins uniform. MSTest soutenir un async ThrowsException, mais seulement pour unité de magasin Windows tester des projets. xUnit a introduit un async ThrowsAsync dans les versions bêta de xUnit 2.0.0.

NUnit est plus complexe. A ce jour, NUnit prend en charge le code asynchrone dans ses méthodes de vérification telles que Assert.Throws. Cependant, afin que cela fonctionne, NUnit présente un SynchronizationContext, qui présente les mêmes problèmes qu'async Sub unit tests. En outre, la syntaxe est actuellement fragile, comme l'exemple dans Figure 2 montre. NUnit envisage déjà d'abandonner le soutien pour les tests d'unité Sub async, et je ne serais pas surpris si ce soutien est déposé en même temps. En Résumé : Je recommande que vous n'utilisez pas cette approche.

Figure 2 NUnit cassantes tests d'Exception

[Test]
public void FailureTest_AssertThrows()
{
  // This works, though it actually implements a nested loop,
  // synchronously blocking the Assert.Throws call until the asynchronous
  // FailAsync call completes.
  Assert.Throws<Exception>(async () => await SystemUnderTest.FailAsync());
}
// Does NOT pass.
[Test]
public void BadFailureTest_AssertThrows()
{
  Assert.Throws<Exception>(() => SystemUnderTest.FailAsync());
}

Ainsi, le soutien actuel pour un prêt async ThrowsException/lancers n'est pas grand. Dans mon propre unité de test de code, j'utilise un type très semblable à la AssertEx en Figure 3. Ce type est plutôt simpliste car il lève juste des objets Exception nus au lieu de faire des affirmations, mais ce même code fonctionne dans l'unité de toutes les grandes infrastructures de test.

Figure 3 la classe AssertEx pour tester les Exceptions de façon asynchrone

using System;
using System.Threading.Tasks;
public static class AssertEx
{
  public static async Task<TException> 
    ThrowsAsync<TException>(Func<Task> action,
    bool allowDerivedTypes = true) where TException : Exception
  {
    try
    {
      await action();
    }
    catch (Exception ex)
    {
      if (allowDerivedTypes && !(ex is TException))
        throw new Exception("Delegate threw exception of type " +
          ex.GetType().Name + ", but " + typeof(TException).Name +
          " or a derived type was expected.", ex);
      if (!allowDerivedTypes && ex.GetType() != typeof(TException))
        throw new Exception("Delegate threw exception of type " +
          ex.GetType().Name + ", but " + typeof(TException).Name +
          " was expected.", ex);
      return (TException)ex;
    }
    throw new Exception("Delegate did not throw expected exception " +
      typeof(TException).Name + ".");
  }
  public static Task<Exception> ThrowsAsync(Func<Task> action)
  {
    return ThrowsAsync<Exception>(action, true);
  }
}

Cela permet à async tâche des tests unitaires à utiliser un ThrowsAsync plus moderne au lieu de la ExpectedExceptionAttribute, comme ceci :

[TestMethod]
public async Task FailureTest_AssertEx()
{
  var ex = await AssertEx.ThrowsAsync(() 
    => SystemUnderTest.FailAsync());
}

Simulacres et Async Stubs

À mon avis, seulement le plus simple du code peut être testée sans une sorte de talon, simulé, faux ou tel autre dispositif. Dans cet article d'introduction, j'appellerai simplement à l'ensemble de ces tests assistants comme simulacres. Lorsque vous utilisez des objets factices, il est utile de programme aux interfaces plutôt que des implémentations. Méthodes asynchrones fonctionnent parfaitement bien avec les interfaces ; le code dans Figure 4 montre comment le code peut utiliser une interface avec une méthode asynchrone.

Figure 4 utilisation d'une méthode asynchrone d'une Interface

public interface IMyService
{
  Task<int> GetAsync();
}
public sealed class SystemUnderTest
{
  private readonly IMyService _service;
  public SystemUnderTest(IMyService service)
  {
    _service = service;
  }
  public async Task<int> RetrieveValueAsync()
  {
    return 42 + await _service.GetAsync();
  }
}

Avec ce code, il est assez facile de créer une application de test de l'interface et passez-le au système sous test. Figure 5 montre comment tester les trois cas principaux stub : asynchrone succès, échec asynchrone et synchrone succès. Échec et succès asynchrones sont les primaires deux scénarios pour l'essai de code asynchrone, mais il est également important de tester le cas synchrone. C'est parce que l'opérateur await se comporte différemment si son await est déjà terminé. Le code dans Figure 5 utilise le Moq moque cadre pour générer les implémentations de talon.

Figure 5 les implémentations de Stub pour Code asynchrone

[TestMethod]
public async Task RetrieveValue_SynchronousSuccess_Adds42()
{
  var service = new Mock<IMyService>();
  service.Setup(x => x.GetAsync()).Returns(() => Task.FromResult(5));
  // Or: service.Setup(x => x.GetAsync()).ReturnsAsync(5);
  var system = new SystemUnderTest(service.Object);
  var result = await system.RetrieveValueAsync();
  Assert.AreEqual(47, result);
}
[TestMethod]
public async Task RetrieveValue_AsynchronousSuccess_Adds42()
{
  var service = new Mock<IMyService>();
  service.Setup(x => x.GetAsync()).Returns(async () =>
  {
    await Task.Yield();
    return 5;
  });
  var system = new SystemUnderTest(service.Object);
  var result = await system.RetrieveValueAsync();
  Assert.AreEqual(47, result);
}
[TestMethod]
public async Task RetrieveValue_AsynchronousFailure_Throws()
{
  var service = new Mock<IMyService>();
  service.Setup(x => x.GetAsync()).Returns(async () =>
  {
    await Task.Yield();
    throw new Exception();
  });
  var system = new SystemUnderTest(service.Object);
  await AssertEx.ThrowsAsync(system.RetrieveValueAsync);
}

En parlant de se moquer des cadres, il y a un peu de soutien qu'ils peuvent donner à test, aussi bien des unités asynchrone. Considérons un instant ce que devrait être le comportement par défaut d'une méthode, si aucun comportement a été spécifiée. Certains cadres moqueuse (tels que Microsoft Stubs) par défaut à la levée d'une exception ; d'autres (tels que Moq) retourne une valeur par défaut. Une méthode asynchrone retourne une tâche < T > un comportement par défaut de naïve serait de revenir par défaut (tâche < T >), en d'autres termes, une tâche nulle, ce qui provoquera une NullReferenceException.

Ce comportement n'est pas souhaitable. Comportement par défaut pour les méthodes asynchrones plus raisonnable serait de revenir Task.FromResult­(default (t)) — autrement dit, une tâche qui s'achève avec la valeur par défaut de T. Cela permet au système sous test à utiliser la tâche retournée. MOQ mis en place ce style de comportement par défaut pour les méthodes asynchrones MOQ version 4.2. À ma connaissance, a ce jour, c'est la seule bibliothèque moqueuse qui utilise des valeurs par défaut d'async facile comme ça.

Synthèse

Async et attendent l'ont été autour depuis l'introduction de Visual Studio 2012, assez longtemps pour que des pratiques exemplaires à émerger. Infrastructures de tests unitaires et des composants d'assistance tels que de se moquer des bibliothèques convergent vers le soutien constant d'async facile. Test aujourd'hui des unités asynchrone est déjà une réalité, et ça ira encore mieux à l'avenir. Si vous n'avez pas fait récemment, maintenant est le bon moment pour mettre à jour vos infrastructures de tests unitaires et se moquant des bibliothèques afin de vous garantir le meilleur support async.

Infrastructures de tests unitaires convergent loin async Sub unit tests et vers des tests unitaires tâche asynchrone. Si vous avez n'importe quel des tests unitaires Sub async, je recommande que vous les modifiiez aujourd'hui à des tests unitaires tâche asynchrone.

Je m'attends au cours des prochaines années, que vous verrez beaucoup mieux appuyer pour tester les cas d'échec dans les tests unitaires asynchrones. Jusqu'à ce que votre infrastructure de test unitaire a un bon support, je suggère que vous utilisez le type de AssertEx mentionné dans cet article, ou quelque chose de similaire qui est plus adaptée à votre infrastructure de test d'unité particulière.

Bonne test d'unité asynchrone est une partie importante de l'histoire d'async et j'ai hâte de voir ces cadres et bibliothèques adoptent async. Un de mes premiers entretiens de foudre était sur les tests unitaires d'async il y a quelques années lorsque async était encore dans l'aperçu de technologie de communauté, et c'est tellement plus facile de faire ces jours-ci !


Stephen Cleary est un mari, le père et le programmeur vivant dans le nord du Michigan. Il a travaillé avec multithreading et asynchrones programmation pendant 16 ans et a utilisé le soutien async dans Microsoft .NET Framework depuis la première preview de technologie de communauté. Il est l'auteur de « simultanéité dans c# Cookbook"(o ' Reilly Media, 2014).  Sa page d'accueil, y compris son blog, est à stephencleary.com.

Merci à l'expert technique Microsoft suivant d'avoir relu cet article : James McCaffrey