Verwenden von Moq zum Verspotten einer asynchronen Methode für einen Komponententest

178

Ich teste eine Methode für einen Dienst, der einen Webanruf APItätigt. Die Verwendung eines Normalen HttpClientfunktioniert gut für Unit-Tests, wenn ich den Webdienst (der sich in einem anderen Projekt in der Lösung befindet) auch lokal ausführe.

Wenn ich jedoch meine Änderungen einchecke, hat der Build-Server keinen Zugriff auf den Webdienst, sodass die Tests fehlschlagen.

Ich habe einen Weg gefunden, dies für meine Komponententests zu umgehen, indem IHttpClientich eine Schnittstelle erstellt und eine Version implementiert habe, die ich in meiner Anwendung verwende. Für Unit-Tests erstelle ich eine verspottete Version mit einer verspotteten asynchronen Post-Methode. Hier bin ich auf Probleme gestoßen. Ich möchte HttpStatusResultfür diesen speziellen Test ein OK zurückgeben . Für einen weiteren ähnlichen Test werde ich ein schlechtes Ergebnis zurückgeben.

Der Test wird ausgeführt, aber niemals abgeschlossen. Es hängt am Warten. Ich bin neu in asynchroner Programmierung, Delegierten und Moq selbst und habe eine Weile nach SO und Google gesucht, um neue Dinge zu lernen, aber ich kann dieses Problem immer noch nicht überwinden.

Hier ist die Methode, die ich zu testen versuche:

public async Task<bool> QueueNotificationAsync(IHttpClient client, Email email)
{
    // do stuff
    try
    {
        // The test hangs here, never returning
        HttpResponseMessage response = await client.PostAsync(uri, content);

        // more logic here
    }
    // more stuff
}

Hier ist meine Unit-Test-Methode:

[TestMethod]
public async Task QueueNotificationAsync_Completes_With_ValidEmail()
{
    Email email = new Email()
    {
        FromAddress = "[email protected]",
        ToAddress = "[email protected]",
        CCAddress = "[email protected]",
        BCCAddress = "[email protected]",
        Subject = "Hello",
        Body = "Hello World."
    };
    var mockClient = new Mock<IHttpClient>();
    mockClient.Setup(c => c.PostAsync(
        It.IsAny<Uri>(),
        It.IsAny<HttpContent>()
        )).Returns(() => new Task<HttpResponseMessage>(() => new HttpResponseMessage(System.Net.HttpStatusCode.OK)));

    bool result = await _notificationRequestService.QueueNotificationAsync(mockClient.Object, email);

    Assert.IsTrue(result, "Queue failed.");
}

Was mache ich falsch?

Danke für Ihre Hilfe.

mvanella
quelle

Antworten:

348

Sie erstellen eine Aufgabe, starten sie jedoch nie, sodass sie niemals abgeschlossen wird. Starten Sie jedoch nicht nur die Aufgabe, sondern wechseln Sie zu "Verwenden" Task.FromResult<TResult>, um eine bereits abgeschlossene Aufgabe zu erhalten:

...
.Returns(Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK)));

Beachten Sie, dass Sie die tatsächliche Asynchronität nicht auf diese Weise testen. Wenn Sie dies tun möchten, müssen Sie etwas mehr Arbeit leisten, um eine zu erstellen Task<T>, die Sie genauer steuern können. Aber das ist etwas für ein anderer Tag.

Vielleicht möchten Sie auch eine Fälschung verwenden, IHttpClientanstatt alles zu verspotten - es hängt wirklich davon ab, wie oft Sie sie benötigen.

Jon Skeet
quelle
2
Vielen Dank. Das hat super geklappt. Ich dachte, es wäre wahrscheinlich etwas Einfaches, das ich nicht verstand.
Mvanella
2
Betreff: Fake IHttpClient, das habe ich mir überlegt, aber ich musste in der Lage sein, verschiedene HttpStatusCodes für verschiedene Tests zurückzugeben, basierend auf dem erwarteten Verhalten, das von der Web-API zurückkommt, und dies schien mir mehr Kontrolle zu geben.
Mvanella
3
@mvanella: Ja, also würdest du eine Fälschung erstellen, die alles zurückgeben kann, was du willst. Nur etwas zum Nachdenken.
Jon Skeet
133
Für alle, die dies jetzt finden, hat Moq 4.2 eine Erweiterung namens ReturnsAysnc, die genau dies tut.
Stuart Grassie
3
@legacybass Ich kann keinen Link zu einer Dokumentation für sie finden, auch wenn die API - Dokumentation sagt , dass sie gegen v4.2.1312.1622 gebaut sind , das wurde veröffentlicht vor fast genau einem Jahr. Sehen Sie sich dieses Commit an, das einige Tage vor dieser Veröffentlichung vorgenommen wurde. Warum die API-Dokumente nicht aktualisiert werden ...
Stuart Grassie
15

Empfehlen Sie die Antwort von @Stuart Grassie oben.

var moqCredentialMananger = new Mock<ICredentialManager>();
moqCredentialMananger
                    .Setup(x => x.GetCredentialsAsync(It.IsAny<string>()))
                    .ReturnsAsync(new Credentials() { .. .. .. });
DineshNS
quelle
1

Mit Mock.Of<...>(...)for- asyncMethode können Sie verwenden Task.FromResult(...):

var client = Mock.Of<IHttpClient>(c => 
    c.PostAsync(It.IsAny<Uri>(), It.IsAny<HttpContent>()) == Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))
);
b Seite
quelle