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

75

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

Meine Behauptung wird aufgerufen, bevor ich mit dem verspotteten LookUpIdAsync fertig bin. In meinem normalen Code ist das genau das, was ich will. Aber für meinen Unit-Test will ich das nicht.

Ich konvertiere von BackgroundWorker nach Async / Await. Mit Background Worker funktionierte dies korrekt, da ich warten konnte, bis der BackgroundWorker fertig war.

Aber es scheint keine Möglichkeit zu geben, auf eine asynchrone Methode zu warten ...

Wie kann ich diese Methode testen?

Vaccano
quelle

Antworten:

68

Sie sollten vermeiden async void. Nur async voidfür Event-Handler verwenden. DelegateCommandist (logisch) ein Ereignishandler, also können Sie es so machen:

// 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 Unit-Test 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 voidMethode 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 das SynchronizationContextfür Ihr Ansichtsmodell während seiner Lebensdauer.

Stephen Cleary
quelle
Das würde funktionieren. Aber ich möchte lieber nicht zwei Methoden für alle meine asynchronen Lücken haben. Ich habe mir einen Weg ausgedacht (zumindest in meinem Fall). Siehe meine Antwort auf diese Frage, wenn Sie interessiert sind.
Vaccano
Schön - Sie haben den C # -Vorgänger von github.com/btford/zone.js ZoneJS erstellt.
Jochen van Wylick
Was ist der Vorteil des Wartens auf DoLookupCommandImpl (idToLookup) in DoStuff (long idToLookUp)? Was wäre, wenn es ohne Wartezeit aufgerufen würde?
Jacekbe
2
@jacekbe: Bei awaitder Aufgabe werden Ausnahmen beachtet . Wenn Sie es ohne aufrufen await, werden alle Fehler stillschweigend ignoriert.
Stephen Cleary
58

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

Wenn Sie dies einem Unit-Test unterziehen müssen, würde ich empfehlen, es async Taskstattdessen zu einer Methode zu machen. Sie können dann Wait()die Ergebnisse aufrufen , die Sie benachrichtigen, wenn die Methode abgeschlossen ist.

Diese Testmethode, wie sie geschrieben wurde, würde jedoch immer noch nicht funktionieren, da Sie nicht DoStuffdirekt testen , sondern eine testen, DelegateCommanddie sie umschließt. Sie müssten diese Methode direkt testen.

Reed Copsey
quelle
1
Ich kann es nicht ändern, um Task zurückzugeben, da der DelegateCommand dies nicht zulässt.
Vaccano
Es ist sehr wichtig, ein Unit-Test-Gerüst um meinen Code zu haben. Möglicherweise muss ich dafür sorgen, dass alle (wichtigen) "async void" -Methoden BackgroundWorker verwenden, wenn sie nicht Unit-getestet werden können.
Vaccano
1
@ Vaccano Das gleiche würde mit BackgroundWorker passieren - Sie müssen dies nur zu einem async Taskstatt machen async voidund auf die Aufgabe warten ...
Reed Copsey
@Vaccano Sie sollten keine async voidMethoden haben (außer für Event-Handler). async voidWie werden Sie damit umgehen, wenn in der Methode eine Ausnahme ausgelöst wird?
Svick
Ich habe einen Weg gefunden, wie es funktioniert (zumindest für diesen Fall). Siehe meine Antwort auf diese Frage, wenn Sie interessiert sind.
Vaccano
18

Ich habe einen Weg gefunden, dies 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, weil ich Unit-Tests bin, die Aufgabe ersetzen kann, bei der mein asynchroner Aufruf (innerhalb meiner asynchronen Leere) zurückgegeben werden soll. Ich stelle dann einfach sicher, dass die Aufgabe abgeschlossen ist, bevor ich weitermache.

Vaccano
quelle
Nur weil Sie lookupTaskfertig sind, heißt das nicht, dass die zu testende Methode ( DoStuff? Oder DoLookupCommand?) Fertig ist. Es besteht eine geringe Wahrscheinlichkeit, dass die Aufgabe ausgeführt wurde, aber IsSearchShowingnoch nicht auf false gesetzt wurde. In diesem Fall würde Ihre Behauptung fehlschlagen.
Dcastro
Ein einfacher Weg, dies zu beweisen, wäre, Thread.Sleep(2000)vor dem Setzen IsSearchShowing auf false zu setzen.
Dcastro
7

Der einzige Weg, den ich kenne, besteht darin, Ihre async voidMethode in async TaskMethode umzuwandeln

Alex Planchon
quelle
5

Sie können ein AutoResetEvent verwenden, um die Testmethode anzuhalten, bis der asynchrone 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);
}
BalintN
quelle
Beispiel für eine AsyncMethodToTest-Klasse?
Kiquenet
2
Warum nicht einfach Wait () verwenden?
Teoman Shipahi
5

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

Nachdem ich einige Zeit mit einem ähnlichen Problem verbracht hatte, fand ich eine einfache Wartezeit, um 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 in dieser Zeile und wird fortgesetzt, nachdem das Ergebnis fertig ist, sodass wir sofort danach bestätigen können.

Velimir
quelle
2

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

bool res = configuration.InitializeAsync(appConfig).Result;
Assert.IsTrue(res);
Billy Jake O'Connor
quelle
was ist configuration?
Rafael Herscovici
1
@Dementic Ein Beispiel? Oder genauer gesagt eine Instanz eines Objekts mit einem Mitglied InitializeAsync, das eine Aufgabe zurückgibt.
Billy Jake O'Connor
0

Ich hatte ein ähnliches Problem. In meinem Fall bestand die Lösung darin, Task.FromResultim moq-Setup Folgendes zu verwenden .Returns(...):

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

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

Datchung
quelle