Usando Moq para burlarse de un método asincrónico para una prueba unitaria

180

Estoy probando un método para un servicio que realiza una APIllamada web . Usar un normal HttpClientfunciona bien para pruebas unitarias si también ejecuto el servicio web (ubicado en otro proyecto en la solución) localmente.

Sin embargo, cuando reviso mis cambios, el servidor de compilación no tendrá acceso al servicio web, por lo que las pruebas fallarán.

He ideado una forma de evitar esto para mis pruebas unitarias creando una IHttpClientinterfaz e implementando una versión que uso en mi aplicación. Para las pruebas unitarias, realizo una versión simulada completa con un método de publicación asincrónica simulada. Aquí es donde me he encontrado con problemas. Quiero devolver un OK HttpStatusResultpara esta prueba en particular. Para otra prueba similar, devolveré un mal resultado.

La prueba se ejecutará pero nunca se completará. Se cuelga a la espera. Soy nuevo en la programación asincrónica, los delegados y el propio Moq y he estado buscando SO y google durante un tiempo aprendiendo cosas nuevas, pero todavía parece que no puedo superar este problema.

Aquí está el método que estoy tratando de probar:

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
}

Aquí está mi método de prueba de unidad:

[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é estoy haciendo mal?

Gracias por tu ayuda.

mvanella
fuente

Respuestas:

351

Estás creando una tarea pero nunca la comienzas, por lo que nunca se completa. Sin embargo, no solo comience la tarea; en cambio, cambie a usar el Task.FromResult<TResult>que le dará una tarea que ya se ha completado:

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

Tenga en cuenta que no probará la asincronía real de esta manera: si desea hacerlo, debe hacer un poco más de trabajo para crear uno Task<T>que pueda controlar de una manera más precisa ... pero eso es algo para otro día.

También es posible que desee considerar el uso de una falsificación en IHttpClientlugar de burlarse de todo; realmente depende de la frecuencia con la que la necesite.

Jon Skeet
fuente
2
Muchas gracias. Eso funcionó muy bien. Pensé que probablemente era algo simple que no estaba entendiendo.
mvanella
2
Re: Fake IHttpClient, lo consideré pero necesitaba poder devolver diferentes HttpStatusCodes para diferentes pruebas basadas en el comportamiento esperado que regresaba de la API web, y esto parecía darme más control.
mvanella
3
@mvanella: Sí, así que crearías una falsificación que puede devolver lo que quieras. Solo algo para pensar.
Jon Skeet
134
Para cualquiera que encuentre esto ahora, Moq 4.2 tiene una extensión llamada ReturnsAysnc, que hace exactamente esto.
Stuart Grassie
3
@legacybass No puedo encontrar un enlace a ninguna documentación para ello, a pesar de que los documentos de la API dicen que están construidos contra v4.2.1312.1622 que se lanzó hace casi exactamente un año. Vea esta confirmación que se realizó unos días antes de ese lanzamiento. En cuanto a por qué los documentos API no se actualizan ...
Stuart Grassie
17

Recomiende la respuesta de @Stuart Grassie arriba.

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

Con Mock.Of<...>(...)for asyncmethod puedes usar Task.FromResult(...):

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