A pirâmide de testes é uma forma gráfica de demonstrar de forma simples os tipos de testes, seus níveis, velocidade de implementação e complexidade dos testes realizados. Tal relação pode ser feita para nos ajudar a chegar ao custo de implementar e manter cada nível de teste, além de nos fornecer informações de qual nível devemos testar primeiro – e por quê.

Ela é ilustrada no formato de uma pirâmide pois busca dar um direcionamento em relação a quantidade de testes a serem implementados em cada um de seus níveis, justamente de acordo com o custo e complexidade de cada tipo de teste.

Existem algumas divisões diferentes quando trata-se da pirâmide de testes, porém vamos tratar da divisão mais comum, que é representada por 3 níveis, sendo eles:

  • Base: Testes de Unidade
  • Meio: Testes de Integração
  • Topo: Testes Ponta a Ponta (E2E, UI ou Testes de Interface)
A pirâmide de testes, com a base sendo composta por testes de unidade, o meio por teste de integração e o topo por testes end to end.

Os níveis de teste geralmente são caracterizados pelos seus objetivos específicos e seu objeto de teste (ou seja, o que está sendo testado, seja de forma manual ou através de testes automatizados). Vamos ver cada um dos níveis de forma mais detalhada a seguir.

Base: Testes de Unidade

Os testes de unidade, ou comumente chamados de testes unitários, são os testes realizados na menor parte testável de uma aplicação, independentemente da sua interação com outras partes do código. Em um contexto de orientação a objetos, por exemplo, podemos classificar uma unidade como um método público disponível em uma determinada classe.

Por testarem os menores pedaços possíveis do código de forma isolada, os testes unitários tendem a ser extremamente pequenos e de rápida criação e execução. Essa característica permite que caso um dos testes falhe, seja possível saber com precisão o local da falha.

Para que isso seja possível, é necessária a criação de alguns objetos “falsos” que imitam o comportamento do objeto real. Dessa forma não é necessário que as demais unidades sejam reais para que o teste seja executado, podendo assim criar esses objetos falsos para presumir seu valor e testar de forma isolada a unidade em questão.

Leia mais: Introdução aos Test Doubles – Fakes, Mocks, Stubs e afins

Outra vantagem dos testes de unidade é que eles podem guiar a escrita do código, principalmente quando utilizados em um desenvolvimento orientado a testes (Test Driven Development – TDD). Isso ocorre porque o código é escrito com o objetivo de passar pelos testes, o que acaba o tornando mais simples e coeso.

Um ponto importante: as boas práticas sugerem que desenvolvedores sejam os responsáveis pela criação de testes unitários, uma vez que eles sabem melhor do que ninguém a estrutura de seu próprio código. Além disso, existem algumas boas razões para que programadores desenvolvam esse tipo de teste:

  • Testes unitários servirão como uma rede de segurança para quando desenvolvedores precisarem refatorar o código;
  • Esses testes ajudarão a dar contexto para pessoas que não são familiares com o código, como por exemplo novos desenvolvedores entrando no projeto;
  • Quando um bug for reportado, os testes de unidade ajudarão o desenvolvedor a entender como aplicar a melhor solução.

Meio: Testes de Integração

Apesar de todas as vantagens descritas anteriormente sobre os testes de unidade, somente eles não são suficientes para uma boa garantia de qualidade do nosso software. É aí que entram os testes de integração.

Esses testes têm como objetivo testar um conjunto de unidades interagindo entre si. Alguns casos comuns de cobertura de testes de integração são testes realizados na comunicação com o banco de dados, comunicação de interfaces, APIs, micro-serviços.

O fato que é às vezes os testes unitários não são suficientes porque podemos ter duas unidades funcionando conforme esperado, porém no momento em que elas precisam interagir entre si, podem não apresentar um comportamento satisfatório. Tal situação pode ser ilustrada com muito bom humor pela situação abaixo:

Gif de uma porta, na qual há uma fechadura no lado fixo da porta, tornando a fechadura incapaz de cumprir sua função (trancar a porta).
A porta e a fechadura funcionam perfeitamente sozinhas, porém a integração entre elas não demonstrou resultado satisfatório.

Para evitarmos esses tipos de problemas é que são implementados os testes de integração, que testam justamente se as unidades funcionam em conjunto.

Neste momento você deve estar se perguntando: “se estas situações não são cobertas pelos testes de unidade e podem ser atendidas pelos testes de integração, porque não implementamos apenas os testes de integração?”

Para essa questão podemos elencar alguns itens:

  • Por testarem um conjunto de unidades, os testes de integração acabam sendo mais complexos de fazer do que um teste de unidade;
  • Ao testar um bloco maior do código, caso algum teste falhe, exige maior esforço para identificar o motivo da falha e também para a sua manutenção;
  • Possuem uma execução mais lenta em relação aos testes de unidade;
  • Muitas vezes os testes de integração demandam um ambiente completo para sua execução.

Topo: Testes de Ponta a Ponta (E2E)

Os testes de ponta a ponta (testes end to end ou testes de interface) têm como objetivo principal simular o comportamento de um usuário final em nossa aplicação. São testes que simulam o ambiente real, ou seja, iniciam a aplicação ou abrem o navegador, navegam dentro da aplicação, clicam em botões, preenchem formulários e, ao fim, validam se o comportamento apresentado condiz com o que seria esperado para tal funcionalidade. Basicamente, eles buscam garantir o funcionamento de todo um fluxo dentro da aplicação.

Muitas vezes os testes E2E são utilizados como parâmetro para determinar o grau de aderência da aplicação aos seus critérios de aceite, ou seja, tendo como objetivo maior demonstrar que a aplicação funciona conforme o esperado ao invés da identificação de possíveis defeitos.

Porém, assim como ocorre com os testes de integração, os testes E2E possuem um nível de complexidade e tempo para execução ainda mais elevado, fazendo-se necessária uma escolha bastante criteriosa de quais cenários terão cobertura de testes nesse nível. Com isso, é sugerida a implementação de um número menor de testes end to end em relação aos dos demais níveis cobrindo somente os casos principais e mais estratégicos dentro do produto. 

Por exemplo, pense nos fluxos mais prioritários da sua aplicação. Quais são os casos onde algum problema poderia impactar mais o usuário e prejudicar seus objetivos? É mais interessante e estratégico cobrir um fluxo de conversão, por exemplo, pois, se houver alguma falha, as perdas podem ser maiores.

Além disso, essa estratégia evita, por exemplo, que fiquemos por horas aguardando a execução dos testes E2E para a conclusão de um deploy.

Voltando a nossa pirâmide de testes e com base nas informações trazidas anteriormente, podemos chegar à conclusão de que quanto mais próximo da base da pirâmide estiver nosso teste, mais simples será sua implementação e rápida será sua execução, e quanto mais ao topo da pirâmide, maior será sua complexidade e, consequentemente, a execução será mais lenta.

A ilustração abaixo explica o conceito:

Em posse dessas informações, podemos identificar a importância de termos a maior parte de nosso código coberto por testes de unidade, já que eles são mais simples de criar e rápidos de executar, gerando assim um custo mais baixo e apoiando o aumento da cobertura de testes no seu produto.

Já para os testes de integração, reservamos fluxos que não estão sendo cobertos pelos testes de E2E e que também não conseguem ser cobertos como um todo pelos testes de unidade, além de garantirmos que bloco da aplicação estão funcionando devidamente.

Já para os testes E2E, que estão no topo da nossa pirâmide, devemos ter uma quantidade mais restrita e cuidadosamente selecionada, devido seu alto custo de criação e execução. Contudo, eles apresentam uma validação mais completa de um fluxo e devem ser aplicados de maneira estratégica dentro da sua aplicação.