Fazendo Mock da Classe HttpClient

O problema?

Ainda que você não viva no mundo dos micro-serviços, uma hora você vai precisar chamar um serviço REST em outra ponta via HTTP/S, e no caso do AspNetCore, o jeito mais natural de fazê-lo é via a classe HttpClient.

Até aqui, tudo tranquilo, os métodos Get, Post, Put, Patch e Delete (talvez até outros) estão lá para serem usados. O problema é, caso sua implementação precise passar por testes unitários, a classe HttpClient pode lhe trazer problemas, já que não há interface disponível para mock e os métodos acima citados não são virtual.

Note que citei que ela “pode” lhe trazer problemas. Como fazer então para que a classe HttpClient não seja um para seus testes unitários?

Tudo se resume ao SendAsync

Se há algo que é mágico no AspNetCore é que ele é código aberto. Isso mesmo, você ir lá no github e ter acesso ao código fonte de praticamente 100% das classes que você usa no seu projeto. E, felizmente, esse é a caso do HttpClient. Vou deixar o link do fonte no fim do artigo.

Dando uma lida na classe, que é bem escrita por sinal, percebe-se que todos os métodos REST com exceção do Get, chamam um método chamado SendAsync, que recebe como parâmetro um objeto HttpRequestMessage e opcionalmente um CancellationToken.

O HttpRequestMessage precisa basicamente do corpo que você enviaria via Post, Put, Patch e Delete. O ponto de atenção é que somente a versão com o CancellationToken é virtual/override. Se você sabe o que o CancellationToken é, você deveria estar usando-o. Caso não, use CancellationToken.None por agora.

O código de exemplo abaixo faz um Post usando HttpClient:

public class MeuController : ControllerBase
{
	private readonly HttpClient client;

    public MeuController(HttpClient client)
    {
        this.client = client;
    }

	public async Task AlgumPost(Criterios criterios)
	{    
	    // estou ignorando quaisquer configurações de header
	    // também estou assumindo que a classe HttpClient é injetada via construto
		var stringContent = new StringContent(JsonConvert.SerializeObject(criterios), Encoding.UTF8, "application/json");
		var result = await this.client.PostAsync($"url", stringContent, CancellationToken.None);
		return Ok(result);
	}
}

O código é até simples. Agora, o teste unitário que permite você fazer o “mockar” a chamada PostAsync seria o seguinte:

public async Task DeveriaExecutarOSendAsyncAoMenosUmaVez()
{
	// assumindo o uso do Moq e xUnit
	var httpClient = new Mock();
	httpClient
	    .Setup(x => x.SendAsync(It.IsAny(), CancellationToken.None))
	    .ReturnsAsync(new HttpResponseMessage()
	    {
	        StatusCode = HttpStatusCode.OK,
	        Content = new StringContent("{}"),
	    }).Verifiable();

	var controller = new MeuController(
	    httpClient.Object
	);

	var criterios = new Criterios() { };

	var result = await controller.AlgumPost(criterios);

	httpClient.Verify(x => x.SendAsync(It.IsAny(), CancellationToken.None), Times.Exactly(1));
}

O teste acima passa com sucesso! Note que não estamos fazendo Mock do PostAsync, mas do SendAsync já que o primeiro não pode ser “mockado”, mas ele chama o segundo. Caso queira checar que a chamada tenha sido correta, basta usar o CallBack oferecido pelo Moq.

Mas e o Get/GetString/GetStringAsync?

O que fazer então com os Gets? Simples! Não os use! Digo isso com certeza pois há uma alternativa muito mais limpa para ele. O próprio Send/SendAsync!

O problema dos Gets é que não há um meio apropriado de tratar erros. Se o request falhar, uma Exception pode ser disparada ou não.

O substituto ideal seria o seguinte:

// ao invés disso
public async Task ObterConteudo()
{
	var resultado = await this.client.GetStringAsync("https://alguma.api.por.ai");
	return Ok(resultado);
}

// faça isso
public async Task ObterConteudo()
{
	var requestMessage = new HttpRequestMessage(HttpMethod.Get, "https://alguma.api.por.ai");
	var response = await this.client.SendAsync(requestMessage);

	if (response.IsSuccessStatusCode)
	{
	    var resultado = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
	    return Ok(resultado);
	}
	else
	{
	    return StatusCode((int)response.StatusCode, response.Content.ReadAsStringAsync().Result);
	}
}

Desta forma, o retorno dado ao cliente é muito mais “honesto” e de quebra, você ganha a oportunidade de testá-lo apropriadamente.
Bom proveito!

Código fonte da classe HttpClient:

https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs

Aqui uma implementação de código funcionando:

https://github.com/Tomamais/dotnet_mocking_httpclient

Comentários

comentários

Deixe uma resposta