A famigerada pirâmide de testes e a abordagem honeycomb

A famigerada pirâmide de testes e a abordagem honeycomb

No post anterior sobre Testes de Performance, falamos um pouquinho sobre a pirâmide de teste original de Mike Cohn e falamos resumidamente porque gostamos deste modelo. Aqui, iremos entrar um pouco mais em detalhes sobre o que é a pirâmide, porque ela é importante, depois disso falaremos o porquê da pirâmide, na verdade, deveria ser um losango.

A função da pirâmide de testes é basicamente definir níveis de testes e te dar um norte quanto à quantidade de testes que você deveria ter em cada um desses níveis.

Pirâmide de testes

Muitos programadores não escrevem nenhum tipo de teste. A principal razão é que eles não entendem por que deveriam gastar tempo nisso, mas baseado no mesmo princípio da pirâmide, vou explicar a sua importância, porém com uma releitura de onde você deveria colocar esforço de fato. No topo , temos os testes de ponta a ponta, também chamados de end-to-end. O objetivo deles é imitar o comportamento do usuário final nas nossas aplicações ( também chamamos este usuário de usuário sintético). Na base, temos os testes de unidade, onde verificamos o funcionamento da menor unidade de código testável da nossa aplicação. Entre essas duas camadas, temos os testes de integração. A ideia deles é verificar se um conjunto de unidades se comporta da maneira correta, só que de forma menos abrangente do que os testes de ponta a ponta.

Nota: O desenho acima, acrescenta duas camadas a mais em relação à pirâmide original, com os testes de API e os testes de Componentes, que são apenas abstrações de maior ou menos nível que as outras camadas, mas que estão sendo utilizados de maneira recorrente entre as empresas.

Testes end-to-end (e2e)

São testes mais custosos de se criar e de se manter, porém são aqueles que garantem de fato a experiência do usuário que está consumindo seu serviço, pois eles simulam o ambiente real, isto é, usam a aplicação da forma que o usuário faria. Abrem o navegador, clicam em botões, preenchem formulários, tudo para garantir que a experiência é exatamente igual ao que se espera. Você pode ler mais sobre diferentes abstrações de e2e nesse post: Exploring the top of the testing pyramid: End-to-end and user interface testing. Vale lembrar que quando falamos “usuário” não estamos falando necessariamente do cliente que vai acessar seu site ou seu app. Se seu software for uma API, então o usuário é quem vai consumir essa API.

Assim, como eu já disse no parágrafo anterior, este tipo de teste não é a "salvação de todos os seus problemas". E eles são complexos de escrever (tanto em tempo quando na expertise da pessoa que os escreve), são lentos pra rodar dependendo de qual é o fluxo a ser simulado e também possuem um custo alto de manutenção, já que interfaces podem mudar facilmente ao longo do tempo. Por esses motivos, geralmente são testes que cobrem apenas os fluxos principais da aplicação, fluxos críticos ou fluxos que são essenciais para a vida do seu produto.

Quality is free, but only to those who are willing to pay heavily for it. - Tom DeMarco
"A qualidade é gratuita, mas apenas para aqueles que estão dispostos a pagar caro por ela." - Tom DeMarco

Testes de unidade

Do lado totalmente oposto da pirâmide, os testes de unidade verificam o funcionamento da menor unidade de código testável da nossa aplicação, independente da interação dela com outras partes do nosso código.

O conceito do que é uma "unidade" geralmente é visto como um método ou função em particular, mas pode ser vista também como um conjunto de classes/métodos/objetos interagindo entre si. O consenso do que seria o tamanho ideal desta tal unidade é algo visto como um acordo entre os times, mas independente do que você ou o seu time usem como padrão, a unidade vai ser sempre definida como a menor parte testável do seu sistema.

O teste de unidade possui duas grandes vantagens:

• Quando um teste de unidade falha, você sabe exatamente onde está o problema: não precisa nem pensar!
• O teste de unidade é "barato", simples de manter e normalmente fácil de escrever, pois o contexto ali é bem delimitado sobre o que é necessário para testar.

Tá, mas agora você me pergunta... Porque não utilizar sempre testes unitários? A resposta é, que o seu software é um conjunto de unidades, e sem isto funcionando, o seu software não funciona; e claro, o teste de unidade não vai pegar problemas como este.

Testes de Integração

Testes de unidade são muito legais, muito simples e muito rápidos, mas eles não são suficientes.

A porta funciona perfeitamente, a fechadura funciona perfeitamente, mas a integração entre ambas não rolou muito bem.

Podemos ter testado duas unidades que interagem entre si separadamente, usando os testes unitários mencionados acima, e concluído que ambas então funcionando como esperado. Ainda assim, é possível que as duas unidades não funcionem em conjunto como a imagem mostra acima.

Para resolver esse problema, temos os testes de integração, que testam algumas unidades funcionando em conjunto. Diferente dos testes de ponta a ponta, são testes que testam funcionalidades, e não o sistema na visão do usuário.

As implicações disso são que testes de integração são:

  • Mais complicados (de fazer e manter) e demorados que os testes de unidade, por testarem uma funcionalidade inteira;
  • Bem mais simples (de fazer e manter) e rápidos que os testes de ponta a ponta - por testarem uma única funcionalidade de cada vez, sem precisar subir a aplicação inteira.
Testes de integração são baratos o suficiente para construir e garantem bom retorno ao custo de manutenção

Voltando para a pirâmide de testes, é importante sempre lembrar que a base da pirâmide é mais fácil de fazer e mais rápida para rodar, enquanto que o topo é mais difícil e lento.

Com isso em mente, a pirâmide nos mostra a importância de que a maior parte do seu código seja coberto por testes de unidade, já que eles rodam muito rápido e são muito simples (de fazer e manter).

Já o nível de teste mais complexo e demorado (os de ponta a ponta), deve possuir menos testes (assim o deploy não fica travado por mais de 1h enquanto os testes estão sendo rodados).

Os testes de integração existem para os cenários que não podem ser cobertos por testes de ponta a ponta, e para cenários que os testes de unidade já cobrem muito bem (até porque não é necessário ter testes redundantes).

O losango de testes

Bom, agora - que falamos da pirâmide, precisamos falar o porquê de não concordarmos 100% com este modelo. Do ponto de vista moderno, a pirâmide de testes parece excessivamente simplista e, portanto, pode ser enganosa.

Logango de testes ou honeycomb

Outra "pirâmide" que acredito fazer mais sentido, principalmente relacionada à arquitetura de microsserviços é a que o André Schaffer apresentou em seu artigo: Testing of Microservices no blog Spotify Labs testando o honeycomb como uma abordagem para microsserviços (ou o que chamamos aqui sobre losango):

Nesta abordagem, nos concentramos em testes de integração. Queremos ter certeza de que nossos serviços funcionam bem juntos e que os detalhes de implementação deles não são tão importantes. Eles devem ser fáceis de alterar e refatorar sem causar bugs em outros serviços.

Ele menciona que o trade-off é uma diminuição na execução de testes, mas acredita que o tempo é compensado por uma codificação mais rápida e facilidade de manutenção. Eu diria que, nesta abordagem, tratamos os serviços como unidades. Este tipo de teste, em que testamos chamadas (API's) entre serviços é chamado de testes de contrato.

Os testes unitários continuam sendo fundamentais quando falamos de garantir a qualidade de serviços e microsserviços. Eles são o nosso principal olhar de qualidade do ponto de vista técnico, do código, e este olhar é fundamental para a construção de aplicações resilientes e seguras. Nesta abordagem de pequenos blocos independentes, entretanto, se as responsabilidades de cada microsserviço forem granulares o suficiente, é possível que tenhamos uma sobreposição entre os testes de serviços e os testes de unidade.

Ambas abordagens são válidas, mas em cenários diferentes. A aplicação de honeycomb testing (aqui apelidada de losango de testes) mostra maiores vantagens na situação onde se aborda uma arquitetura de microsserviços, aquela que os serviços têm responsabilidades bastante granulares e, portanto, as responsabilidades dos testes unitários e de serviços se aproximam bastante (realidade na qual a Vórtx está inserida). Notem que, mesmo neste contexto, os testes unitários não são descartados, apenas utilizados em menor quantidade, reforçando os testes de casos de uso que de fato agregam valor ao software.

Conclusão

A pirâmide de testes é um ótimo começo para a grande maioria das arquiteturas de software moderno, porém ao longo dos tempos aprendemos aqui que os testes de integração (nossos usecases) geravam mais valor e mais cobertura à falhas, sendo barato o suficiente para escrevê-los e mantê-los.

E é isso! Espero que tenha ficado mais claro os porquês de os testes serem assim e que essa seção de testes tenha dado o devido direcionamento.