Pirâmide de Testes — Definindo uma boa suíte de testes para seu Software

Guilherme Biff Zarelli
luizalabs
Published in
9 min readAug 31, 2020

Objetivo: Nesse artigo falaremos sobre como definir uma boa suíte de testes em um software, apresentar e demonstrar conceitos e boas práticas baseando-se na Pirâmide de Testes de Mike Cohn — com exemplos em Java

Inspiração: Esse artigo é inspirado em situações e dificuldades reais vivenciadas em sistemas que possuem a inversão da pirâmide de testes, dificultando a execução de refatorações e manutenções.

A Pirâmide de Testes é uma ótima metáfora visual que nos diz para agrupar testes de software em diferentes granularidades, Mike Cohn a criou em seu livro Succeeding with Agile que faz você pensar em seus testes em diferentes camadas. Ele também ilustra a quantidade de testes que devem ser realizados em cada camada.

Embora o conceito da pirâmide de teste já exista há algum tempo, as equipes ainda lutam para colocá-la em prática adequadamente.

Mike Cohn’s original test automation pyramid

A pirâmide de testes original de Mike Cohn consiste em três camadas nas quais sua suíte de testes deve ser (de baixo para cima):

  1. Testes unitários
  2. Testes de Serviço
  3. Testes de interface do usuário

Devido à sua simplicidade, a essência da pirâmide serve como uma boa regra geral quando se trata de estabelecer seu próprio conjunto de testes. Sua melhor aposta é lembrar duas coisas da pirâmide original de Cohn:

  • Escreva testes com granularidade diferente
  • Quanto mais alto o nível, menos testes você deve ter

Não fique muito apegado aos nomes das camadas individuais na pirâmide de teste de Cohn. Dadas as deficiências dos nomes originais, é totalmente aceitável criar outros nomes para as camadas de teste, desde que você o mantenha consistente na sua base de código e nas discussões da sua equipe.

Nomenclaturas mais utilizadas

Atenha-se à forma da pirâmide para criar um conjunto de testes saudável, rápido e sustentável. Faça vários testes de unidade pequenos e rápidos. Escreva alguns testes de granulação mais grossa e muito poucos testes de alto nível que testam seu aplicativo de ponta a ponta.

Cuidado para não acabar com uma suíte de testes de casquinha de sorvete que será um pesadelo para manter e levará muito tempo para ser executada.

The Software testing ice-cream cone

Os Testes de Unidade

Os testes de unidade têm o escopo mais restrito de todos os testes em seu conjunto de testes. O número de testes de unidade em seu conjunto de testes superará em grande parte qualquer outro tipo na pirâmide.

Os testes de unidade devem ser executados muito rapidamente, cada teste deve ter como premissa executar pequenos pedaços de sua base de código isoladamente e evitar acessos a base de dados, sistemas de arquivos ou disparar consultas de rede, desconecte recursos externos, configure alguns dados de entrada, olhe para o assunto que está em teste e verifique se o valor retornado é o que você esperava tudo isso vai garantir que seus testes permaneçam com a velocidade de execução adequada.

Os testes unitários devem garantir que todos os seus caminhos de código não triviais sejam testados, porém eles não devem estar muito ligado à sua implementação. Os testes devem atuar como uma rede de segurança para alterações de código, por isso não devemos refletir a estrutura interna do código neles, teste o comportamento observável.

Não é correto pensar em um teste da seguinte maneira:

“Se eu digitar y , o método chamará a classe A primeiro, depois chamará a classe B e retornará o resultado da classe A mais o resultado da classe B?”

Pense:

“Se eu inserir os valores y , o resultado será z ?”

Uma classe de teste de unidade deve pelo menos testar a interface pública da classe. Métodos privados geralmente devem ser considerados um detalhe de implementação e não devem ser testados. Sempre que surge essa situação, geralmente chega-se à conclusão de que a classe que estamos testando já é muito complexa e está fazendo muito, violando o princípio da responsabilidade única — o S dos cinco princípios do SOLID. Uma das soluções para esse problema é dividir essa classe para que esse método privado que é tão crucial para ser testado se torne um método público de uma nova classe e assim usaremos da composição para incluir a funcionalidade em nossa classe. Além de melhorar a estrutura do código sua classe não violará o princípio de responsabilidade única;

Estruturação dos Testes

Para uma boa estruturação de seus testes podemos aplicar uma regra simples que irá deixar seu código muito mais legível.

  1. Configure os dados de teste (given)
  2. Chame seu método em teste (when)
  3. Afirme que os resultados esperados são retornados (then)

Lembre-se sempre: “given”, “when”, “then” (dado isso, quando feito isso, então verifique isso)

Esse padrão também pode ser aplicado a outros testes de mais alto nível. Em todos os casos, eles garantem que seus testes permaneçam fáceis e consistentes de ler. Além disso, os testes escritos com essa estrutura em mente tendem a ser mais curtos e mais expressivos.

Spring Framework e Teste de Unidade

O Spring fornece suporte a testes de unidade, porém, não confunda. A estrutura fornecida para testes de unidade do Spring é apenas a respeito de mocks e algumas classes de utilidades fornecidas pelo package ‘org.springframework.test.util’. A grande maioria das ferramentas disponíveis pelo Spring foram desenvolvidas para auxiliar os testes de integração, por exemplo, quando você faz um teste de que utiliza a notação @RunWith com o SpringRunner ou a @SpringBootTest você está realizando um teste de integração.

Testes de Integração

Os testes de integração determinam se as unidades de software desenvolvidas independentemente funcionam corretamente quando estão conectadas umas às outras. O objetivo do teste de integração, como o nome sugere, é testar se muitos módulos desenvolvidos separadamente funcionam juntos conforme o esperado . Isso foi realizado ativando muitos módulos e executando testes de nível superior contra todos eles para garantir que eles operassem juntos.

Escreva testes de integração para todos os trechos de código em que você serializa ou desserializa dados. Isso acontece com mais frequência do que você imagina nas seguintes situações:

  • Chamadas à API REST dos seus serviços
  • Lendo e gravando em bancos de dados
  • Chamando APIs de outros aplicativos
  • Lendo e gravando em filas
  • Escrevendo no sistema de arquivos

Escrever testes de integração em torno desses limites garante que a gravação e a leitura de dados desses colaboradores externos funcionem bem.

Testando a integração com a base de dados

Esse tipo de teste não deve ser executado em seu banco de dados de produção, homologação ou algo do tipo, o adequado é configurarmos um banco de dados mais leve, um H2 em disco ou memória por exemplo, ou subirmos uma instância em um container isolado, se atente para a criação/execução desses testes para que não haja dependências de dados entre eles.

  1. Iniciar um banco de dados
  2. Conecte seu aplicativo ao banco
  3. Acionar uma função dentro do seu código que grave dados no banco de dados
  4. Verifique se os dados esperados foram gravados no banco de dados lendo os dados do banco de dados

Testando uma integração com uma API externa

Nesse teste note que usaremos um recurso do Spring Framework que simula nosso serviço, assim como um banco de dados isolado, o WireMockRule permite simularmos nosso endpoint para que ele retorne o que configurarmos nele, assim conseguimos testar adequadamente como nosso sistema se comportaria com determinada resposta.

  1. Inicie seu aplicativo
  2. Iniciar uma instância do serviço separado (ou um teste duplo com a mesma interface)
  3. Acionar uma função dentro do seu código que lê da API do serviço separado
  4. Verifique se seu aplicativo pode analisar a resposta corretamente

Testando seus próprios Controllers

Teste seus Controllers, assim como os outros testes, também temos que testar nossos controllers para garantimos a entrada dos dados e os retornos adequados para quem o consome. Nesse tipo de teste diferente dos outros, não precisamos simular o repositório ou a API externa, podemos utilizar o mock para garantir a entrega dos dados fornecendo um teste integrado do controller com seu service;

Spring Framework e Testes de Integração

O Spring Framework disponibiliza diversas ferramentas para nos auxiliar nos testes de integração, facilitando mockar recursos de chamadas, injetar dependências, e executar em cima da nossa aplicação. Com a ajuda dos profiles também conseguimos executar testes com arquivos de configurações específicos, permitindo até alterar as configurações de uma base de dados para executarmos nossos testes adequadamente;

Além das diversas outras notações disponíveis no framework:

@BootstrapWith
@ContextConfiguration
@WebAppConfiguration
@ContextHierarchy
@ActiveProfiles
@TestPropertySource
@DirtiesContext
@TestExecutionListeners
@Commit
@Rollback
@BeforeTransaction
@AfterTransaction
@Sql
@SqlConfig
@SqlMergeMode
@SqlGroup

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/testing.html#integration-testing

Testes End-to-End

O teste de ponta a ponta é uma metodologia de teste de software para testar um fluxo de aplicativo do início ao fim. O objetivo deste teste é simular um cenário real do usuário e validar o sistema em teste e seus componentes para integração e integridade dos dados.

Devido ao alto custo de manutenção, você deve reduzir o número de testes end-to-end ao mínimo possível. Pense nas interações de alto valor que os usuários terão com seu aplicativo. Tente criar cenários do usuário que definem o valor principal do seu produto e traduza as etapas mais importantes desses cenários em testes automatizados end-to-end.

Os testes end-to-end também exigem muita manutenção e sua execução normalmente é lenta. Pensando em um cenário com mais de dois microsserviços dificilmente você poderá executar todos seus testes localmente, pois exigirá iniciar todos os microsserviços localmente utilizando muito recurso, o que acaba deixando seus testes frágeis e dependentes de ambientes de homologação que podem estar desatualizados.

Lembre-se: Em sua pirâmide de testes você tem muitos níveis mais baixos, onde você já testou todos os tipos de casos extremos e integrações com outras partes do sistema. Não há necessidade de repetir esses testes em um nível superior. Um alto esforço de manutenção e muitos falsos positivos o atrasam e fazem com que você perca a confiança em seus testes, mais cedo ou mais tarde.

Para qualquer versão comercial do software, os testes end-to-end desempenha um papel importante, pois testa todo o aplicativo em um ambiente que imita exatamente os usuários do mundo real.

Teste End-to-End com Spring Framework

O Spring Framework também nos fornece recursos para realizar testes End-to-End para fazer requisições HTTP e asserts diretamente em seu endpoint.

Se você precisar iniciar um servidor em execução, recomendamos o uso de portas aleatórias. Se você usar @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT), uma porta disponível será selecionada aleatoriamente toda vez que seu teste for executado.

Por conveniência, os testes que precisam fazer chamadas REST podemos utilizar a classe TestRestTemplate.

Também pode-se injetar repositórios nos testes end-to-end para salvar valores que auxiliem em sua execução, o importante é testar realmente a aplicação de ponta a ponta. Não faça mocks nessa camada.

Conclusão

Ter uma suíte de testes sólidos exigem muito esforço, porém nunca será uma perda de tempo, eles irão garantir toda regra de negócio de seu sistema, a médio longo prazo tornará seu software muito mais seguro.

Testes automatizados são fundamentais para um desenvolvimento de qualidade, e todo desenvolvedor tem como obrigação desenvolvê-los. Eles trazem diversos benefícios como a diminuição de erros durante o processo de codificação, aumento de qualidade do projeto, revisão de lógica entre outros.

Uma boa granulação entre os diversos tipos de testes ilustrado na pirâmide garantem uma cobertura quase perfeita entre os diversos cenários de execução de seu software eles também auxiliam na manutenção e evolução do sistema pois com uma boa suíte de testes desenvolvedores com menos experiência na regra de negócio do projeto terão uma segurança muito maior ao realizar o desenvolvimento de novas funcionalidades.

--

--