Utilisation de Moq pour simuler une méthode asynchrone pour un test unitaire

180

Je teste une méthode pour un service qui effectue un APIappel Web . L'utilisation d'un normal HttpClientfonctionne bien pour les tests unitaires si j'exécute également le service Web (situé dans un autre projet de la solution) localement.

Cependant, lorsque j'enregistre mes modifications, le serveur de compilation n'aura pas accès au service Web, donc les tests échoueront.

J'ai imaginé un moyen de contourner ce problème pour mes tests unitaires en créant une IHttpClientinterface et en implémentant une version que j'utilise dans mon application. Pour les tests unitaires, je crée une version simulée complète avec une méthode de publication asynchrone simulée. Voici où j'ai rencontré des problèmes. Je veux retourner un OK HttpStatusResultpour ce test particulier. Pour un autre test similaire, je retournerai un mauvais résultat.

Le test s'exécutera mais ne se terminera jamais. Il s'accroche à l'attente. Je suis nouveau dans la programmation asynchrone, les délégués et Moq lui-même et je cherche depuis un certain temps SO et Google pour apprendre de nouvelles choses, mais je n'arrive toujours pas à surmonter ce problème.

Voici la méthode que j'essaye de tester:

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
}

Voici ma méthode de test unitaire:

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

Qu'est-ce que je fais mal?

Merci de votre aide.

mvanella
la source

Réponses:

351

Vous créez une tâche mais ne la démarrez jamais, donc elle ne se termine jamais. Cependant, ne vous contentez pas de démarrer la tâche - au lieu de cela, Task.FromResult<TResult>passez à using qui vous donnera une tâche qui est déjà terminée:

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

Notez que vous ne testerez pas l'asynchronie réelle de cette façon - si vous voulez faire cela, vous devez faire un peu plus de travail pour créer un Task<T>que vous pouvez contrôler de manière plus fine ... mais c'est quelque chose pour un autre jour.

Vous pouvez également envisager d'utiliser un faux IHttpClientplutôt que de vous moquer de tout - cela dépend vraiment de la fréquence à laquelle vous en avez besoin.

Jon Skeet
la source
2
Merci beaucoup. Cela a très bien fonctionné. J'ai pensé que c'était probablement quelque chose de simple que je ne comprenais pas.
mvanella
2
Re: Fake IHttpClient, j'ai considéré cela mais je devais être en mesure de renvoyer différents HttpStatusCodes pour différents tests en fonction du comportement attendu en retour de l'API Web, et cela semblait me donner plus de contrôle.
mvanella
3
@mvanella: Oui, vous créez donc un faux qui peut renvoyer ce que vous voulez. Juste quelque chose à penser.
Jon Skeet
134
Pour tous ceux qui trouvent cela maintenant, Moq 4.2 a une extension appelée ReturnsAysnc, qui fait exactement cela.
Stuart Grassie
3
@legacybass Je ne trouve pas de lien vers une documentation à ce sujet, même si la documentation de l'API indique qu'elles sont construites avec la v4.2.1312.1622 qui a été publiée il y a presque exactement un an. Voir ce commit qui a été effectué quelques jours avant cette version. Quant à savoir pourquoi les documents de l'API ne sont pas mis à jour ...
Stuart Grassie
17

Recommandez la réponse de @Stuart Grassie ci-dessus.

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

Avec la méthode Mock.Of<...>(...)pour async, vous pouvez utiliser Task.FromResult(...):

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