web-dev-qa-db-ger.com

Warten Sie auf einen Async Void Methodenaufruf für Unit-Tests

Ich habe eine Methode, die so aussieht:

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

Ich versuche es so zu testen:

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

Mein Assert wird aufgerufen, bevor ich mit dem verspotteten LookUpIdAsync fertig bin. In meinem normalen Code möchte ich genau das. Aber für meinen Unit-Test will ich das nicht.

Ich konvertiere zu Async/Await von BackgroundWorker. Mit Background Worker funktionierte dies einwandfrei, da ich auf den BackgroundWorker warten konnte.

Es scheint jedoch nicht möglich zu sein, auf eine asynchrone Void-Methode zu warten ...

Wie kann ich diese Methode testen?

57
Vaccano

Ich habe einen Weg gefunden, es für Unit-Tests zu tun:

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

Der Schlüssel hier ist, dass ich, da ich Unit-Tests durchführe, die Aufgabe ersetzen kann, zu der ich meinen asynchronen Anruf (in meinem asynchronen Leerzeichen) zurückgeben möchte. Ich vergewissere mich dann, dass die Aufgabe abgeschlossen ist, bevor ich weitermache.

16
Vaccano

Sie sollten async void vermeiden. Verwenden Sie async void nur für Ereignishandler. DelegateCommand ist (logisch) ein Event-Handler, also können Sie dies folgendermaßen tun:

// 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);
}

und Gerätetest als:

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

Meine empfohlene Antwort ist oben. Wenn Sie eine async void-Methode wirklich testen möchten, können Sie dies mit meiner AsyncEx-Bibliothek tun:

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

Diese Lösung ändert jedoch die SynchronizationContext für Ihr Ansichtsmodell während seiner Lebensdauer.

53
Stephen Cleary

Eine async void-Methode ist im Wesentlichen eine "Feuer und Vergessen" -Methode. Es gibt keine Möglichkeit, ein Abschlussereignis zurück zu bekommen (ohne ein externes Ereignis usw.).

Wenn Sie einen Gerätetest durchführen müssen, würde ich empfehlen, stattdessen eine async Task-Methode zu erstellen. Sie können dann Wait() für die Ergebnisse aufrufen, um Sie darüber zu informieren, wenn die Methode abgeschlossen ist.

Diese Testmethode würde jedoch immer noch nicht funktionieren, da Sie DoStuff nicht direkt testen, sondern eine DelegateCommand testen, die sie umschließt. Sie müssen diese Methode direkt testen.

50
Reed Copsey

Sie können ein AutoResetEvent verwenden, um die Testmethode anzuhalten, bis der async-Aufruf abgeschlossen ist:

[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

Die angegebene Antwort testet den Befehl und nicht die Async-Methode. Wie oben erwähnt, benötigen Sie einen weiteren Test, um diese asynchrone Methode zu testen.

Nachdem ich einige Zeit mit einem ähnlichen Problem verbracht hatte, fiel es mir leicht, eine asynchrone Methode in einem Komponententest zu testen, indem ich einfach synchron aufrief:

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

und die Verwendung:

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

Der Test wartet auf dieser Zeile und wird fortgesetzt, nachdem das Ergebnis fertig ist, so dass wir sofort danach bestätigen können.

4
Velimir

Die einzige Möglichkeit, die ich kenne, ist, Ihre async void-Methode auf async Task-Methode umzustellen

4
Alex Planchon

Ich hatte ein ähnliches Problem. In meinem Fall bestand die Lösung darin, Task.FromResult im moq-Setup für .Returns(...) wie folgt zu verwenden:

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

Alternativ hat Moq auch eine ReturnsAysnc(...) -Methode.

0
datchung

Ändern Sie Ihre Methode, um eine Task zurückzugeben, und Sie können Task.Result verwenden

bool res = configuration.InitializeAsync(appConfig).Result;
Assert.IsTrue(res);
0