No artigo anterior expliquei como o Flurl permite construir URLs, fazer chamadas HTTP e testá-las de forma muito mais legível. Também comentei que infelizmente a parte de testes do Flurl não funcionava com o HttpClient
, mas isso é só parcialmente verdade, porque depois de investir um tempo lendo o código do Flurl, consegui enganá-lo para que sua estrutura de testes funcionasse com o HttpClient
, e é isso que vou mostrar neste artigo.
O Flurl é muito interessante para projetos com chamadas HTTP, mas pegamos vários projetos com código pré-existente, e isso faz com que projetos que utilizam apenas o HttpClient
, sem o Flurl, sejam quase unanimidade, e acredito que essa é a realidade de muitos projetos.
Mas e se eu quiser utilizar somente a parte de testes do Flurl para escrever testes de unidade para o HttpClient
de forma mais legível e mais simples?
Calma, tá tudo bem agora.
Flurl Testing
Recapitulando, o Flurl permite simular respostas de um servidor e ainda fazer asserts em cima de propriedades do Request para garantir que o código está chamando corretamente a API que está sendo consumida.
Como exemplo, vou usar uma classe que usa o HttpClient
para fazer chamadas para uma API. Para este artigo usarei a API pública FakeJSON – ela é capaz de gerar dados fake a partir de um template passado. Criei uma classe para encapsular essa chamada da seguinte maneira:
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace HttpTestFlurl
{
public class FakeJsonService
{
private readonly HttpClient _httpClient;
public FakeJsonService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetFakeData()
{
var templateDados = @"{
""token"": ""4UByP0KUbLPL_KMIZNWRbg"",
""data"": {
""id"": ""personNickname"",
""email"": ""internetEmail"",
""last_login"": {
""date_time"": ""dateTime|UNIX"",
""ip4"": ""internetIP4""
}
}
}";
var response = await _httpClient
.PostAsync("https://app.fakejson.com/q",
new StringContent(templateDados, Encoding.UTF8
, "application/json"));
return await response.Content.ReadAsStringAsync();
}
}
}
Repare que minha classe recebe o HttpClient
como parâmetro. É assim que eu vou conseguir trocar o HttpClient
real por um fake quando for executar meus testes.
Quero ser capaz de garantir as seguintes informações: chamo a URL correta, com o método HTTP correto, com os headers corretos, com corpo correto, e que a chamada é feita apenas uma vez.
Sem utilizar os helpers de testes do Flurl eu consigo fazer quase tudo isso dessa maneira:
request.RequestUri.Should().Be(new Uri("https://app.fakejson.com/q"));
request.Method.Should().Be(HttpMethod.Post);
request.Content.Headers.ContentType.MediaType.Should().Be("application/json");
(await request.Content.ReadAsStringAsync()).Should().BeEquivalentTo(templateDados);
Fica muito complexo garantir que a chamada foi feita apenas uma vez, por isso resolvi nem fazer isso no assert acima.
Agora veja como fica o mesmo teste com os helpers do Flurl, com o bônus de conseguir garantir a quantidade de chamadas feitas:
httpTest.ShouldHaveCalled("https://app.fakejson.com/q")
.WithVerb(HttpMethod.Post)
.WithContentType("application/json")
.WithRequestBody(templateDados)
.Times(1);
Bem melhor, né? Mas, calma.
Só sair escrevendo os asserts com o HttpTest
do Flurl em cima do HttpClient
não vai funcionar: a chamada ainda não está sendo mockada. Ou seja, a chamada vai bater no servidor do FakeJSON, o que tornaria o teste integrado, algo que eu não quero neste momento. Portanto, preciso trocar o HttpClient
por um fake.
Além disso, mesmo que eu utilize um HttpClient
fake para o teste, o HttpTest
do Flurl não está configurado para logar nenhuma das chamadas feitas pelo HttpClient
, porque ele não foi feito para isso. Como solucionar esses dois problemas, então?
HttpClient + Flurl Testing
Como o Flurl é open source, abri o seu código para entender porque os testes não funcionavam com o HttpClient
. Depois de várias tentativas, descobri que é preciso basicamente fazer um wrapper para a classe de FlurlRequest
a cada chamada do HttpClient
.
O HttpClient possui uma estrutura peculiar. Na verdade, ele é basicamente um helper para construção de requests. O envio das chamadas em si é todo feito por uma classe interna, do tipo HttpMessageHandler.
Então, para fazer qualquer tipo de alteração no envio de requests, é só implementar um novo HttpMessageHandler
e passá-lo para o HttpClient
, que aceita um handler no construtor.
Pensando nisso, criei um FakeHttpClientMessageHandler
que seja capaz de enganar o Flurl para ser usado nos testes:
using Flurl.Http;
using Flurl.Http.Content;
using Flurl.Http.Testing;
using System;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace HttpTestFlurl.Tests
{
public class FakeHttpClientMessageHandler : FakeHttpMessageHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var flurlRequest = new FlurlRequest(request.RequestUri.ToString());
var stringContent = (request.Content as StringContent);
if (stringContent != null)
request.Content = new CapturedStringContent(await stringContent.ReadAsStringAsync(), GetEncodingFromCharSet(stringContent.Headers?.ContentType?.CharSet), stringContent.Headers?.ContentType?.MediaType);
if (request?.Properties != null)
request.Properties["FlurlHttpCall"] = new HttpCall()
{
FlurlRequest = flurlRequest,
Request = request
};
return await base.SendAsync(request, cancellationToken);
}
private Encoding GetEncodingFromCharSet(string charset)
{
try
{
return HasQuote(charset)
? Encoding.GetEncoding(charset.Substring(1, charset.Length - 2))
: Encoding.GetEncoding(charset);
}
catch (ArgumentException)
{
return null;
}
}
private bool HasQuote(string text)
=> text.Length > 2 && text[0] == '\"' && text[text.Length - 1] == '\"';
}
}
Com esse handler é possível utilizar toda a estrutura de testes do Flurl com o HttpClient nativo do .NET. Só é preciso instanciá-lo e utilizá-lo na construção do HttpClient
, então meu teste ficou assim:
using Flurl.Http.Testing;
using HttpTestFlurl;
using HttpTestFlurl.Tests;
using NUnit.Framework;
using System.Net.Http;
using System.Threading.Tasks;
namespace Tests
{
public class FakeJsonServiceTests
{
private FakeJsonService _fakeJsonService;
[Test]
public async Task GetFakeDataShouldCallFakeJsonServerWithCorrectParameters()
{
using (var httpTest = new HttpTest())
{
_fakeJsonService = new FakeJsonService(new HttpClient(new FakeHttpClientMessageHandler()));
var templateDados = @"{
""token"": ""4UByP0KUbLPL_KMIZNWRbg"",
""data"": {
""id"": ""personNickname"",
""email"": ""internetEmail"",
""last_login"": {
""date_time"": ""dateTime|UNIX"",
""ip4"": ""internetIP4""
}
}
}";
var response = await _fakeJsonService.GetFakeData();
httpTest.ShouldHaveCalled("https://app.fakejson.com/q")
.WithVerb(HttpMethod.Post)
.WithContentType("application/json")
.WithRequestBody(templateDados)
.Times(1);
}
}
}
}
Pronto! Agora o teste passa, mesmo que eu não esteja fazendo a chamada com o Flurl.Http.
Os códigos que mostrei possuem algumas simplificações para tornar o artigo mais direto. No seu projeto você provavelmente deve se preocupar com algumas coisas a mais, como utilizar uma Factory de HttpClient para a classe de Service, colocar o HttpTest no SetUp/TearDown do seu teste, entre outros detalhes.
Conclusão
Utilizando o handler que criei é possível fazer testes de unidade para o HttpClient
sem a necessidade de utilizar o Flurl.Http no seu projeto. Literalmente o Flurl só existe no seu projeto de testes e mais nada.
Essa dica pode ser bem útil se você está num cenário como o meu, onde já existe um projeto com HttpClient
e você não tem o tempo para refatorar tudo agora, mas quer adicionar testes de unidade.
Assim o impacto no código de produção é o mínimo possível inicialmente e você pode ir refatorando aos poucos, já colhendo os benefícios de testes simplificados com o Flurl.
Happy testing!
***
Este artigo foi produzido em parceria com a Lambda3. Leia outros conteúdos no blog da empresa: blog.lambda3.com.br