91 votes

Attendre l'appel d'une méthode Async Void pour les tests unitaires

J'ai une méthode qui ressemble à ceci :

private async void DoStuff(long idToLookUp)
{
    IOrder order = await orderService.LookUpIdAsync(idToLookUp);   

    // Close the search
    IsSearchShowing = false;
}    

//Other stuff in case you want to see it
public DelegateCommand<long> DoLookupCommand{ get; set; }
ViewModel()
{
     DoLookupCommand= new DelegateCommand<long>(DoStuff);
}    

J'essaie de le tester de la manière suivante :

[TestMethod]
public void TestDoStuff()
{
    //+ Arrange
    myViewModel.IsSearchShowing = true;

    // container is my Unity container and it setup in the init method.
    container.Resolve<IOrderService>().Returns(orderService);
    orderService = Substitute.For<IOrderService>();
    orderService.LookUpIdAsync(Arg.Any<long>())
                .Returns(new Task<IOrder>(() => null));

    //+ Act
    myViewModel.DoLookupCommand.Execute(0);

    //+ Assert
    myViewModel.IsSearchShowing.Should().BeFalse();
}

Mon assert est appelé avant que j'aie terminé le LookUpIdAsync simulé. Dans mon code normal, c'est exactement ce que je veux. Mais pour mon test d'unité, je ne veux pas cela.

Je suis en train de passer de l'utilisation de BackgroundWorker à Async/Await. Avec le BackgroundWorker, cela fonctionnait correctement car je pouvais attendre que le BackgroundWorker se termine.

Mais il ne semble pas y avoir de moyen d'attendre une méthode async void...

Comment puis-je tester cette méthode à l'unité ?

80voto

Stephen Cleary Points 91731

Vous devez éviter async void . N'utiliser que async void pour les gestionnaires d'événements. DelegateCommand est (logiquement) un gestionnaire d'événements, vous pouvez donc procéder comme suit :

// Use [InternalsVisibleTo] to share internal methods with the unit test project.
internal async Task DoLookupCommandImpl(long idToLookUp)
{
  IOrder order = await orderService.LookUpIdAsync(idToLookUp);   

  // Close the search
  IsSearchShowing = false;
}

private async void DoStuff(long idToLookUp)
{
  await DoLookupCommandImpl(idToLookup);
}

et le tester en tant que :

[TestMethod]
public async Task TestDoStuff()
{
  //+ Arrange
  myViewModel.IsSearchShowing = true;

  // container is my Unity container and it setup in the init method.
  container.Resolve<IOrderService>().Returns(orderService);
  orderService = Substitute.For<IOrderService>();
  orderService.LookUpIdAsync(Arg.Any<long>())
              .Returns(new Task<IOrder>(() => null));

  //+ Act
  await myViewModel.DoLookupCommandImpl(0);

  //+ Assert
  myViewModel.IsSearchShowing.Should().BeFalse();
}

La réponse que je recommande se trouve ci-dessus. Mais si vous voulez vraiment tester un async void vous pouvez le faire avec ma méthode Bibliothèque AsyncEx :

[TestMethod]
public void TestDoStuff()
{
  AsyncContext.Run(() =>
  {
    //+ Arrange
    myViewModel.IsSearchShowing = true;

    // container is my Unity container and it setup in the init method.
    container.Resolve<IOrderService>().Returns(orderService);
    orderService = Substitute.For<IOrderService>();
    orderService.LookUpIdAsync(Arg.Any<long>())
                .Returns(new Task<IOrder>(() => null));

    //+ Act
    myViewModel.DoLookupCommand.Execute(0);
  });

  //+ Assert
  myViewModel.IsSearchShowing.Should().BeFalse();
}

Mais cette solution modifie la SynchronizationContext pour votre modèle de vue pendant sa durée de vie.

64voto

Reed Copsey Points 315315

Un async void est essentiellement une méthode de type "feu et oubli". Il n'existe aucun moyen de récupérer un événement d'achèvement (sans événement externe, etc.).

Si vous devez effectuer des tests unitaires, je vous recommande d'en faire un module async Task à la place. Vous pouvez alors appeler Wait() sur les résultats, qui vous informera de l'achèvement de la méthode.

Cependant, cette méthode de test, telle qu'elle est rédigée, ne fonctionnerait toujours pas, car vous ne testez pas réellement les éléments suivants DoStuff directement, mais plutôt en testant un DelegateCommand qui l'enveloppe. Vous devez tester cette méthode directement.

18voto

Vaccano Points 18515

J'ai trouvé un moyen de le faire pour les tests unitaires :

[TestMethod]
public void TestDoStuff()
{
    //+ Arrange
    myViewModel.IsSearchShowing = true;

    // container is my Unity container and it setup in the init method.
    container.Resolve<IOrderService>().Returns(orderService);
    orderService = Substitute.For<IOrderService>();

    var lookupTask = Task<IOrder>.Factory.StartNew(() =>
                                  {
                                      return new Order();
                                  });

    orderService.LookUpIdAsync(Arg.Any<long>()).Returns(lookupTask);

    //+ Act
    myViewModel.DoLookupCommand.Execute(0);
    lookupTask.Wait();

    //+ Assert
    myViewModel.IsSearchShowing.Should().BeFalse();
}

La clé ici est que, parce que je fais des tests unitaires, je peux substituer dans la tâche que je veux que mon appel asynchrone (à l'intérieur de mon vide asynchrone) renvoie. Je m'assure alors que la tâche est terminée avant de passer à autre chose.

10voto

Alex Planchon Points 139

Le seul moyen que je connaisse est de tourner votre async void à la méthode async Task método

5voto

BalintN Points 23

Vous pouvez utiliser un événement AutoResetEvent pour arrêter la méthode de test jusqu'à ce que l'appel asynchrone soit terminé :

[TestMethod()]
public void Async_Test()
{
    TypeToTest target = new TypeToTest();
    AutoResetEvent AsyncCallComplete = new AutoResetEvent(false);
    SuccessResponse SuccessResult = null;
    Exception FailureResult = null;

    target.AsyncMethodToTest(
        (SuccessResponse response) =>
        {
            SuccessResult = response;
            AsyncCallComplete.Set();
        },
        (Exception ex) =>
        {
            FailureResult = ex;
            AsyncCallComplete.Set();
        }
    );

    // Wait until either async results signal completion.
    AsyncCallComplete.WaitOne();
    Assert.AreEqual(null, FailureResult);
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X