web-dev-qa-db-fra.com

Attendre un async Void appel de méthode 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 un peu comme ceci:

[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 assertion est appelée avant que je finisse avec la maquette LookUpIdAsync. Dans mon code normal, c'est exactement ce que je veux. Mais pour mon test d'unité, je ne le veux pas.

Je suis en train de convertir en Async/Await en utilisant BackgroundWorker. Avec un arrière-plan, cela fonctionnait correctement car je pouvais attendre que BackgroundWorker se termine.

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

Comment puis-je tester cette méthode?

57
Vaccano

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 suis en train de tester les unités, je peux substituer à la tâche le retour de mon appel asynchrone (dans mon vide asynchrone). Je vérifie ensuite que la tâche est terminée avant de continuer.

16
Vaccano

Vous devriez éviter async void. Utilisez uniquement async void pour les gestionnaires d'événements. DelegateCommand est (logiquement) un gestionnaire d'événements, vous pouvez donc le faire 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 testez l'unité comme:

[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();
}

Ma réponse recommandée est ci-dessus. Mais si vous voulez vraiment tester une méthode async void, vous pouvez le faire avec ma 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 de votre modèle de vue pendant sa durée de vie.

53
Stephen Cleary

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

Si vous avez besoin de tester cela à l'unité, je vous conseillerais plutôt d'utiliser une méthode async Task. Vous pouvez ensuite appeler Wait() sur les résultats pour vous avertir de la fin de la méthode.

Cependant, cette méthode de test telle qu'elle est écrite ne fonctionnerait toujours pas, car vous ne testez pas DoStuff directement, mais plutôt une DelegateCommand qui l'encapsule. Vous auriez besoin de tester cette méthode directement.

50
Reed Copsey

Vous pouvez utiliser AutoResetEvent pour arrêter la méthode de test jusqu'à la fin de l'appel asynchrone:

[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);
}
5
BalintN

La réponse fournie teste la commande et non la méthode async. Comme mentionné ci-dessus, vous aurez besoin d'un autre test pour tester cette méthode asynchrone.

Après avoir passé du temps avec un problème similaire, j'ai trouvé une attente facile pour tester une méthode asynchrone dans un test unitaire en appelant simplement de manière synchrone:

    protected static void CallSync(Action target)
    {
        var task = new Task(target);
        task.RunSynchronously();
    }

et l'utilisation:

CallSync(() => myClass.MyAsyncMethod());

Le test attend sur cette ligne et continue une fois que le résultat est prêt afin que nous puissions affirmer immédiatement après.

4
Velimir

Le seul moyen que je connaisse est de transformer votre méthode async void en méthode async Task

4
Alex Planchon

J'ai eu un problème similaire. Dans mon cas, la solution consistait à utiliser Task.FromResult dans la configuration du moq pour .Returns(...) comme suit:

orderService.LookUpIdAsync(Arg.Any<long>())
    .Returns(Task.FromResult(null));

Moq propose également une méthode ReturnsAysnc(...).

0
datchung

Changez votre méthode pour retourner une tâche et vous pouvez utiliser Task.Result

bool res = configuration.InitializeAsync(appConfig).Result;
Assert.IsTrue(res);
0
Billy Jake O'Connor