Użycie Moq do wyszydzenia metody asynchronicznej dla testu jednostkowego

179

Testuję metodę usługi, która wykonuje APIpołączenie internetowe . Używanie normalnego HttpClientdziała dobrze w testach jednostkowych, jeśli lokalnie uruchamiam również usługę internetową (znajdującą się w innym projekcie w rozwiązaniu).

Jednak po wpisaniu zmian serwer kompilacji nie będzie miał dostępu do usługi sieci Web, więc testy zakończą się niepowodzeniem.

Rozwiązałem ten problem w przypadku testów jednostkowych, tworząc IHttpClientinterfejs i implementując wersję używaną w mojej aplikacji. Do testów jednostkowych wykonuję próbną wersję z próbną metodą postu asynchronicznego. Tutaj napotkałem problemy. Chcę zwrócić OK HttpStatusResultdla tego konkretnego testu. Do kolejnego podobnego testu zwrócę zły wynik.

Test zostanie uruchomiony, ale nigdy się nie zakończy. Wisi na oczekiwaniu. Jestem nowy w programowaniu asynchronicznym, delegatach i samym Moq i od dłuższego czasu szukam SO i Google, ucząc się nowych rzeczy, ale nadal nie mogę sobie poradzić z tym problemem.

Oto metoda, którą próbuję przetestować:

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
}

Oto moja metoda testu jednostkowego:

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

Co ja robię źle?

Dziękuję za pomoc

mvanella
źródło

Odpowiedzi:

350

Tworzysz zadanie, ale nigdy go nie uruchamiasz, więc nigdy się nie kończy. Nie uruchamiaj jednak zadania - zamiast tego zmień na użycie, Task.FromResult<TResult>które da ci zadanie, które zostało już ukończone:

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

Pamiętaj, że nie będziesz testować w ten sposób faktycznej asynchronii - jeśli chcesz to zrobić, musisz wykonać trochę więcej pracy, aby stworzyć Task<T>kontroler, który możesz kontrolować w bardziej szczegółowy sposób ... ale to jest coś dla kolejny dzień.

Możesz również rozważyć użycie fałszywego IHttpClientzamiast wyśmiewać wszystko - to naprawdę zależy od tego, jak często go potrzebujesz.

Jon Skeet
źródło
2
Dziękuję Ci bardzo. To działało świetnie. Uznałem, że to prawdopodobnie coś prostego, czego nie rozumiałem.
mvanella,
2
Re: Fałszywy IHttpClient, zastanowiłem się nad tym, ale musiałem móc zwrócić różne kody HttpStatusCode dla różnych testów opartych na oczekiwanym zachowaniu wracającym z interfejsu API sieci Web, i to wydawało się dać mi większą kontrolę.
mvanella,
3
@mvanella: Tak, więc stworzyłbyś podróbkę, która może zwrócić cokolwiek chcesz. Po prostu coś do przemyślenia.
Jon Skeet,
134
Dla każdego, kto znajdzie to teraz, Moq 4.2 ma rozszerzenie o nazwie ReturnsAysnc, które właśnie to robi.
Stuart Grassie,
3
@legacybass Nie mogę znaleźć linku do żadnej dokumentacji, chociaż dokumentacja API mówi, że są one zbudowane przeciwko wersji 4.2.1312.1622, która została wydana prawie dokładnie rok temu. Zobacz to zatwierdzenie, które zostało dokonane na kilka dni przed wydaniem. Dlaczego dokumenty API nie są aktualizowane ...
Stuart Grassie
16

Polecam odpowiedź @Stuart Grassie powyżej.

var moqCredentialMananger = new Mock<ICredentialManager>();
moqCredentialMananger
                    .Setup(x => x.GetCredentialsAsync(It.IsAny<string>()))
                    .ReturnsAsync(new Credentials() { .. .. .. });
DineshNS
źródło
1

Dzięki Mock.Of<...>(...)za asyncmetody można użyć Task.FromResult(...):

var client = Mock.Of<IHttpClient>(c => 
    c.PostAsync(It.IsAny<Uri>(), It.IsAny<HttpContent>()) == Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))
);
Strona B
źródło