Não há dúvidas que a presença de bons testes é indispensável para garantir a qualidade de um microserviço. O que muita gente não se dá conta, é que algumas categorias de testes podem ser nocivas à produtividade de um time e devem ser evitadas. Existem também categorias de teste cujo retorno sobre investimento é absurdamente grande. Neste artigo, exploro esse cenário a fim de demonstrar quais são os melhores tipos de teste para aplicar em microserviços.
Nos últimos três anos, venho trabalhando com microserviços na AmbevTech, e uma coisa foi ficando clara ao longo do caminho: a clássica pirâmide de testes é inadequada para microserviços.
Quando concebida, no início dos anos 2000, a ideia era que testes de unidade eram mais fáceis de escrever, e mais rápidos de rodar do que outros tipos, como os testes de integração. Isso foi antes do Docker, antes dos microserviços. Era outra realidade, ainda governada pelas grandes aplicações.
Mas quando um microserviço tem apenas algumas poucas milhares de linhas de código, ele é tão pequeno que os testes de unidade acabam inevitavelmente acoplados aos detalhes de implementação. Tornam-se testes anêmicos.
Testes anêmicos
Testes anêmicos são aqueles que, para funcionarem corretamente, dependem em demasia da configuração de mocks. O uso exagerado de mocks é um anti-pattern conhecido como Mockery.
Testes anêmicos costumam apresentar as seguintes características:
-
São difíceis de manter
-
Podem ser difíceis compreender
-
Tendem a quebrar com frequência
-
Desencorajam refatorações
Este último ponto é bastante importante: testes anêmicos quebram por qualquer coisinha. Uma pequena refatoração bem intencionada pode quebrar testes simplesmente porque alguns deles estavam mais preocupados com o funcionamento interno de uma classe do que com o resultado de uma operação.
Bons testes devem suportar e incentivar refatorações. Afinal, a prática da refatoração é desejável em qualquer projeto, visto que é vital para manter a saúde da base de código ao longo da vida do projeto.
Assim, se os testes quebram até mesmo em refatorações que não afetam nem o comportamento nem o resultado dos processos, então esses testes estão no caminho, mais atrapalhando que ajudando.
Um bom teste deveria dar ao desenvolvedor segurança de que sua refatoração não está mudando a regra de negócio acidentalmente. Se o teste quebra durante uma refatoração, torna-se inútil, e talvez eliminá-lo seja melhor no longo prazo.
Aparentemente, no Spotify eles evitam chamar esses testes de "testes de unidade", preferindo o termo "testes de detalhes de implementação".
Muitas vezes, a relação de acoplamento entre a suíte de testes e a aplicação terá o seguinte aspecto:
Qual a melhor unidade de teste?
Em projetos que utilizam design orientado a objetos, existe uma tendência de tratar as classes como as unidades que devem ser testadas. Já em projetos procedurais ou funcionais, existe a tendência de tratar as funções como unidade de teste.
Mas como esse artigo do Fabio Pereira aponta, as verdadeiras unidades são os comportamentos. Esta é uma afirmação importante, porque nos indica a importância do BDD (Behavior Driven Develpment) como potencial prática para resolver esse impasse.
No livro Test Driven Development: By Example, do Kent Beck, podemos encontrar uma boa definição de testes de unidade. Ele os define como "testes que são independentes um dos outros – significando que a execução de um não deve afetar os outros".
Ou seja, não têm nada a ver com testar classes ou funções, mas com testar unidades isoladas de forma determinística: toda vez que rodarmos um teste, o resultado deverá ser consistentemente o mesmo.
A utilização do BDD converge naturalmente para uma resolução desse impasse: testar cada comportamento isolado dos outros, com foco em entradas e saídas, nos leva a testes que serão independentes dos detalhes de implementação.
Testes focados em comportamento costumam ter as seguintes características:
-
São fáceis de manter
-
São fáceis de compreender
-
São resilientes a refatorações
-
Encorajam refatorações
São basicamente, a antítese dos testes anêmicos, e possuem justamente as características que buscamos num ambiente de desenvolvimento ágil e que visa qualidade e produtividade.
Neste artigo do Spotify sobre testes de microserviços, eles afirmam que os testes de integração são os que oferecem a melhor relação custo x benefício para microserviços:
Estes dois artigos do Kent C. Dodds também discutem esse tema no mundo das aplicações JavaScript e são leituras bem interessantes:
Testes desse tipo tenderão a ser, como o Martin Fowler costuma chamar, testes sociáveis [1]:
Testes de integração como forma preferível de testar
Testes de integração que aplicam BDD corretamente costumam ser bastante fáceis de compreender e possuem baixo custo de manutenção. Eles também se mantêm fiéis à regra de negócio por mais tempo.
Ainda segundo o mesmo artigo do Spotify, os testes por lá não costumam ser mais complexos do que isso:
Em vários projetos na AmbevTech, empregamos a biblioteca MediatR como forma de padronizar a invocação de interações com a aplicação, facilitando a escrita de testes com essas características. Nossos testes normalmente têm a seguinte aparência:
public class ListagemBonificacoesTests : ApplicationTestBase
{
[Fact]
public async Task Deve_ordenar_pelo_id_decrescente_quando_nao_for_especificada_nenhuma_ordenacao()
{
InsertMany(ListaBonificacoesComIdsSequencias());
ListarBonificacoesPaginadasRequest requestSemOrdenacao = new();
ListaBonificacoesPaginadasResponse response = await Handle<ListarBonificacoesPaginadasRequest>(requestSemOrdenacao);
var bonificacoes = response.Items;
bonificacoes.Should().BeInDescendingOrder();
}
}
Em todo caso, é interessante que a forma de interagir com a aplicação sejam padronizadas, permitindo assim que os testes foquem nessas interações com entradas determinadas, e se restrinjam a verificar as saídas e eventuais efeitos colaterais, como registros inseridos no banco de dados ou mensagens publicadas em um broker.
Seguindo esses passos, teremos uma suíte de testes confiável e bastante desacoplada dos detalhes de implementação, como ilustrado na figura abaixo:
Conclusões
Como vimos, testes de unidade em microserviços tendem a gerar acoplamento com os detalhes de implementação, e por isso seu uso deve ser reservado para componentes pontuais de alta complexidade, ou que sejam difíceis de testar por outros meios, como uma chamada a um serviço externo.
Já a forma preferível de testar microserviços são os testes de integração focados em comportamento. A aplicação de técnicas de BDD traz inúmeros benefícios: em especial, a facilidade de manutenção e confiabilidade, provendo suporte para refatorações e evolução dos serviços.
Essas conclusões são fruto dos meus estudos e reflexões ao longo dos últimos anos sobre esse assunto que eu gosto tanto, e espero que sejam úteis para você. Agora, leitor, te convido a deixar um comentário abaixo. Obrigado pela leitura!