A Pirâmide de Testes

A “Pirâmide de Teste” é uma metáfora que nos diz para agrupar testes de software em diferentes granularidades. Também dá uma idéia de quantos testes devemos fazer em cada um desses grupos. Embora o conceito da pirâmide de teste já exista há algum tempo, as equipes ainda lutam para colocá-la em prática adequadamente.

Se você deseja levar a sério os testes automatizados para o seu software, existe um conceito-chave que você deve conhecer: A pirâmide de teste de Mike Cohn surgiu com esse conceito em seu livro Succeeding with Agile, é uma ótima metáfora visual dizendo para você pensar em diferentes camadas de teste. Ele também informa quanto teste deve ser feito em cada camada.

A pirâmide de teste 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 de teste 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 de teste original de Cohn:

  1. Escreva testes com granularidade diferente
  2. 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.

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 casquinha de sorvete de teste que será um pesadelo para manter e levará muito tempo para ser executada.

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 de teste.

Discussão: Teste de unidade deve ser totalmente isolados utilizando mocks ou stubs, ou devem interagir com outras classes?

O que importa afinal é escrever os testes, pode-se usar as duas abordagem, o que não pode ocorrer é permitir que os testes unitários se tornem lentos, as duas abordagens são válidas.

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 melhoramos o princípio de responsabilidade única.

Estruturando os testes

Uma boa estrutura para todos os seus testes (isso não se limita aos testes de unidade) é esta:

  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.

   @Test
    public void shouldReturnFullNameOfAPerson() throws Exception {
	   //given
        Person peter = new Person("Peter", "Pan");
        when(personRepo).findByLastName("Pan"))
            .doReturn(Optional.of(peter));

        //when
        String greeting = subject.hello("Pan");
       
        //then      
        assertThat(greeting, is("Hello Peter Pan!"));
    }

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, ou seja, quando você faz um teste de ‘unidade’ que utiliza a notação @RunWith com o SpringRunner ou @SpringBootTest você provavelmente está realizando um teste de integração.

Conclusão

  • Fácil legibilidade
  • Execução rápida
  • Sem muitas integrações com outras classes (utilização de mocks / stubs)
  • Atente-se ao uso de Frameworks
  • Deve possuir a maior parte do coverage de acordo com a pirâmide de testes

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 as 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 ou algo do tipo, o adequado é configurarmos um banco de dados mais leve, um H2 em disco 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
@RunWith(SpringRunner.class)
@DataJpaTest
public class PersonRepositoryIntegrationTest {
    @Autowired
    private PersonRepository subject;
 
    @After
    public void tearDown() throws Exception {
        subject.deleteAll();
    }
 
    @Test
    public void shouldSaveAndFetchPerson() throws Exception {
        Person peter = new Person("Peter", "Pan");
        subject.save(peter);
 
        Optional<Person> maybePeter = subject.findByLastName("Pan");
 
        assertThat(maybePeter, is(Optional.of(peter)));
    }
}

Testando uma integração com uma API externa

Nesse teste note que usaremos um recurso do Spring 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
@RunWith(SpringRunner.class)
@SpringBootTest
public class WeatherClientIntegrationTest {

    @Autowired
    private WeatherClient subject;

    @Rule
    public WireMockRule wireMockRule = new WireMockRule(8089);

    @Test
    public void shouldCallWeatherService() throws Exception {
        wireMockRule.stubFor(get(urlPathEqualTo("/some-test-api-key/53.5511,9.9937"))
                .willReturn(aResponse()
                        .withBody(FileLoader.read("classpath:weatherApiResponse.json"))
                        .withHeader(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withStatus(200)));

        Optional<WeatherResponse> weatherResponse = subject.fetchWeather();

        Optional<WeatherResponse> expectedResponse = Optional.of(new WeatherResponse("Rain"));
        assertThat(weatherResponse, is(expectedResponse));
    }
}

Testando seus próprios Controllers

Teste seus Controllers, assim como os testes acima, também temos que testar nossos controllers, assim 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;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = ExampleController.class)
public class ExampleControllerAPITest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private PersonRepository personRepository;

    @MockBean
    private WeatherClient weatherClient;

    @Test
    public void shouldReturnFullName() throws Exception {
        Person peter = new Person("Peter", "Pan");
        given(personRepository.findByLastName("Pan")).willReturn(Optional.of(peter));

        mockMvc.perform(get("/hello/Pan"))
                .andExpect(content().string("Hello Peter Pan!"))
                .andExpect(status().is2xxSuccessful());
    }

    @Test
    public void shouldReturnCurrentWeather() throws Exception {
        WeatherResponse weatherResponse = new WeatherResponse("Hamburg, 8°C raining");
        given(weatherClient.fetchWeather()).willReturn(Optional.of(weatherResponse));

        mockMvc.perform(get("/weather"))
                .andExpect(status().is2xxSuccessful())
                .andExpect(content().string("Hamburg, 8°C raining"));
    }
}

Spring Framework e Testes de Integração

O Spring Framework disponibiliza diversas ferramentas para nos auxiliar nos testes de integração, nos exemplos acima por exemplo, conseguimos de maneira bem simples mockar recursos de chamadas, injetar dependências, e executar em cima da nossa aplicação; Com a ajuda dos profiles, conseguimos também executar os testes com arquivos de configurações específicos, permitindo até alterar as configurações de uma base de dados (para executar em um H2 por exemplo) 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

Conclusão

Os testes de integração devem garantir a comunicação com as pontas do sistema (Banco, Api’s, Mensageria, Controller, etc). Os testes nessa camada tendem a ser mais lentos por normalmente utilizarem sistemas de arquivos ou até mesmo ter que subir toda a instancia de sua aplicação para executa-los, por isso e outros motivos os frameworks são muito utilizados nessa camada para deixar o acesso a recursos mais fáceis permitindo um aumento de produtividade.

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 o 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.

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 pode-se injetar como argumento do método 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.

 Conforme mostrado no exemplo a seguir:

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SampleE2ETests {

    @Autowired 
    private TestRestTemplate restTemplate;

    @Autowired
    private PersonRepository personRepository;

    @After
    public void tearDown() throws Exception {
        personRepository.deleteAll();
    }

    @Test
    void exampleTest() {
        Person peter = new Person("Peter", "Pan");
        personRepository.save(peter);

        String body = restTemplate.getForObject("/hello/Pan", String.class);
        assertThat(body).isEqualTo("Hello Peter Pan!");
    }
}

Conclusão

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, como comunicação em rede, interação com o banco de dados etc.

Referências

  • https://martinfowler.com/articles/practical-test-pyramid.html
  • https://watirmelon.blog/testing-pyramids/
  • https://martinfowler.com/bliki/UnitTest.html
  • https://martinfowler.com/bliki/GivenWhenThen.html
  • http://xunitpatterns.com/Four%20Phase%20Test.html
  • https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/testing.html
  • https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing
  • https://martinfowler.com/bliki/IntegrationTest.html
  • https://www.softwaretestinghelp.com/what-is-integration-testing/
  • https://github.com/hamvocke/spring-testing
  • https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/testing.html#integration-testing
  • https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/web/client/TestRestTemplate.html
  • https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server

Repositórios de referência

Help DEV – Analista desenvolvedor Java / Android https://helpdev.com.br/zarelli

A Pirâmide de Testes

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.

Rolar para o topo