O melhor e o pior tipo de testes para microserviços

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.

Pirâmide de testes tradicional, com muitos testes de unidade na base, quantidade mediana de testes de integração no meio, e poucos testes end to end no topo
Figura 1. Pirâmide de testes tradicional

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:

O diagrama mostra duas suítes de teste, com várias classes de teste. Cada classe de teste está correlacionada com uma das classes da aplicação, gerando alto acoplamento.
Figura 2. Alto acoplamento

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:

Favo de mel de testes de microserviços
Figura 3. Favo de mel de testes de 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 sociáveis e testes solitários
Figura 4. Testes sociáveis são preferíveis em microserviços

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:

Exemplo de teste de integração no Spotify
Figura 5. Exemplo de teste de integração no Spotify

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:

Exemplo de teste de integração num dos times da AmbevTech
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:

Bom relacionamento entre os módulos de teste e da aplicação, com cada suíte de teste acessando apenas um entrypoint do módulo de aplicação
Figura 6. Baixo acoplamento

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.

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!


1. Quem criou os termos "testes sociáveis" e "testes solitários" foi Jay Fields, no livro "Working Effectively with Unit Tests"
Outras opções para compartilhar:
comments powered by Disqus