O HttpClient é uma biblioteca muito utilizada nos sistemas .Net. Em um resumo muito breve, ela é responsável por fazer requisições HTTP de forma assíncrona. Mas o problema aparece quando precisamos criar testes mockados para nossa aplicação, isto porque o HttpClient é uma classe concreta e não uma interface (como o IHttpClientFactory).
Neste artigo vamos mostrar como é possível criar mocks para a classe HttpClient e definir o comportamento que desejamos para validar nossos testes.
O Projeto
Neste projeto vamos escrever um método utilizando o HttpClient fazendo uma chamada GET para uma url. Em seguida vamos escrever os testes mockando o HttpClient e inserindo o retorno que queremos receber para validar o método escrito.
O HttpClient
Resumindo, o HttpClient é utilizado para enviar e receber solicitações HTTP. Cada HttpClient instanciado cria e usa seu próprio pool de conexões, isolando suas solicitações executadas por outras instâncias de HttpClient. Para informações mais detalhadas é aconselhável sempre ler a boa e velha Documentação HttpClient.
Antes de seguirmos vale lembrar que é sempre aconselhável utilizar o IHttpClientFactory para abstrair as chamadas Http, mas neste artigo vamos falar sobre HttpClient porque é muito comum trabalharmos com sistemas legados e que, na maioria das vezes, dificulta muito a refatoração do código, principalmente por não ter testes.
Podemos saber mais sobre o IHttpClientFactory neste documento.
Criando um HttpClient GET
Vamos criar um método GetAsync() que fará uma chamada do tipo GET para uma url.
Neste método vamos retornar uma lista de strings quando a chamada HttpClient for executada com sucesso. Caso aconteça algum problema na chamada HTTP e ela retornar qualquer StatusCode diferente de sucesso (família 200) vamos criar um log e retornar uma lista vazia.
Por fim, vamos colocar em um bloco try/catch para capturar a exception do tipo HttpRequestException.
Vamos lembrar que nosso foco aqui é a criar um método simples para o nosso objetivo, criar um mock da classe HttpClient. Neste exemplo não vamos focar em boas práticas que fogem do nosso tema.
É muito importante notarmos que estamos passando o HttpClient pelo construtor. Somente assim é possível mockarmos a classe.
public class ExternalService
{
private readonly HttpClient _httpClient;
private const string _baseUrl = "https://codigomaromba.com/";
private readonly ILogger<ExternalService> _logger;
public ExternalService(
HttpClient httpClient,
ILogger<ExternalService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<IEnumerable<string>> GetAsync()
{
try
{
using var response = await _httpClient.GetAsync($"{_baseUrl}endpoint-path").ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("A chamada retornou uma resposta inesperada.");
return Enumerable.Empty<string>();
}
var content = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<IEnumerable<string>>(content);
return result;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Erro ao realizar a chamada.");
throw new ArgumentOutOfRangeException(ex.Message, new ApplicationException());
}
}
}
Mockando o HttpClient
Com nosso método criado e pronto para ser testado, vamos criar a classe de teste para validarmos todos as possibilidades de execução do nosso método.
public class ExternalServiceTests
{
[Fact]
public async Task GetAsync_Success()
{
// Arrange
var httpMessageHandlerMock = new Mock<HttpMessageHandler>();
var httpResponseMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(@"[ ""string 1"", ""string 2"", ""string 3"", ""string 4"" ]"),
};
httpMessageHandlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(httpResponseMessage);
var httpClient = new HttpClient(httpMessageHandlerMock.Object);
var externalService = new ExternalService(httpClient, Mock.Of<ILogger<ExternalService>>());
// Act
var response = await externalService.GetAsync().ConfigureAwait(false);
// Assert
Assert.NotEmpty(response);
Assert.Contains("string 1", response);
Assert.Contains("string 2", response);
Assert.Contains("string 3", response);
Assert.Contains("string 4", response);
}
[Fact]
public async Task GetAsync_Failure()
{
// Arrange
var httpMessageHandlerMock = new Mock<HttpMessageHandler>();
var httpResponseMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.NotFound,
Content = new StringContent(@"[]"),
};
httpMessageHandlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(httpResponseMessage);
var httpClient = new HttpClient(httpMessageHandlerMock.Object);
var externalService = new ExternalService(httpClient, Mock.Of<ILogger<ExternalService>>());
// Act
var response = await externalService.GetAsync().ConfigureAwait(false);
// Assert
Assert.Empty(response);
}
[Fact]
public void GetAsync_Exception()
{
// Arrange
var httpMessageHandlerMock = new Mock<HttpMessageHandler>();
httpMessageHandlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ThrowsAsync(new HttpRequestException());
var httpClient = new HttpClient(httpMessageHandlerMock.Object);
var externalService = new ExternalService(httpClient, Mock.Of<ILogger<ExternalService>>());
// Act
var response = new Func<Task>(() => externalService.GetAsync());;
// Assert
Assert.ThrowsAsync<ArgumentOutOfRangeException>(response);
}
}
Neste teste estamos testando todas as possibilidades. Estamos testando o caso de sucesso com o GetAsync_Success(), o caso de retorno diferente de sucesso GetAsync_Failure() e até mesmo a exception GetAsync_Exception().
Agora temos 100% de cobertura de teste do nosso método que utiliza HttpClient para fazer as requisições HTTP.
Resultado
Com este exemplo vimos que é possível criarmos mock para chamadas HttpClient, seja para qual caso for. Os mesmos podem ser utilizados para fazer chamadas do tipo POST, DELETE, PUT, etc…
Executando o Code Coverage do Visual Studio podemos ver que todos os casos foram cobertos e validados por testes.

Conclusão
Como dito anteriormente, se fossemos criar um projeto do zero o ideal seria utilizar o IHttpClientFactory, mas é muito comum tratarmos de sistemas “legados” e muitas vezes não temos a oportunidade de refatorar, mas nem por isto não devemos escrever testes para os nossos sistemas. Até porque eu não aconselho refatorar um sistema que não tenha testes, é a receita do fracasso.
Os fontes deste artigo podem ser encontrados no meu GitHub.
Show!!!
CurtirCurtido por 1 pessoa