Usando o Moq para simular um método assíncrono para um teste de unidade

180

Estou testando um método para um serviço que faz uma APIchamada pela Web . Usar um normal HttpClientfunciona bem para testes de unidade, se eu também executar o serviço da Web (localizado em outro projeto na solução) localmente.

No entanto, ao fazer o check-in de minhas alterações, o servidor de compilação não terá acesso ao serviço da web, portanto os testes falharão.

Eu criei uma maneira de contornar isso para meus testes de unidade, criando uma IHttpClientinterface e implementando uma versão que eu uso no meu aplicativo. Para testes de unidade, faço uma versão simulada completa com um método de postagem assíncrona simulada. Aqui é onde eu encontrei problemas. Eu quero retornar um OK HttpStatusResultpara este teste específico. Para outro teste semelhante, retornarei um resultado ruim.

O teste será executado, mas nunca será concluído. Trava na espera. Eu sou novo na programação assíncrona, delegados e no próprio Moq e tenho pesquisado o SO e o Google por um tempo aprendendo coisas novas, mas ainda não consigo superar esse problema.

Aqui está o método que estou tentando testar:

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
}

Aqui está o meu método de teste de unidade:

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

O que estou fazendo de errado?

Obrigado pela ajuda.

mvanella
fonte

Respostas:

351

Você está criando uma tarefa, mas nunca a iniciando, portanto nunca está sendo concluída. No entanto, não basta iniciar a tarefa; em vez disso, mude para using, Task.FromResult<TResult>que fornecerá uma tarefa que já foi concluída:

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

Observe que você não testará a assincronia real dessa maneira - se você quiser fazer isso, precisará trabalhar um pouco mais para criar um Task<T>que possa controlar de maneira mais refinada ... mas isso é algo para outro dia.

Você também pode considerar o uso de uma falsificação para IHttpClientnão zombar de tudo - isso realmente depende de quantas vezes você precisa.

Jon Skeet
fonte
2
Muito obrigado. Isso funcionou muito bem. Achei que provavelmente era algo simples que eu não estava entendendo.
mvanella
2
Re: Fake IHttpClient, eu considerei isso, mas eu precisava ser capaz de retornar HttpStatusCodes diferentes para testes diferentes com base no comportamento esperado que volta da API da Web, e isso parecia me dar mais controle.
mvanella
3
@mvanella: Sim, então você criaria um falso que pode retornar o que você quiser. Apenas algo para se pensar.
Jon Skeet
134
Para quem encontra isso agora, o Moq 4.2 tem uma extensão chamada ReturnsAysnc, que faz exatamente isso.
Stuart Grassie
3
@legacybass Não consigo encontrar um link para nenhuma documentação para isso, mesmo que os documentos da API digam que foram criados contra a v4.2.1312.1622, lançada quase exatamente um ano atrás. Veja este commit feito alguns dias antes do lançamento. Quanto ao motivo pelo qual os documentos da API não são atualizados ...
Stuart Grassie
17

Recomende a resposta de @Stuart Grassie acima.

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

Com Mock.Of<...>(...)a asyncmétodo que pode utilizar Task.FromResult(...):

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