Pirâmide de Testes: Conhecendo os testes locais

Henrique Luis Schmidt
Sicredi Tech
Published in
5 min readApr 5, 2024

TL;DR (Resumo)

Continuando nossa série sobre a filosofia de testes do Sicredi (leia a parte anterior), mergulhamos na base da pirâmide, onde estão os testes locais, abordando:

  • Testes Unitários: Verificação rápida e isolada de componentes do código, utilizando mocks.
  • Testes de Integração Local: Garantia de funcionamento nas classes que fazem a integração com o mundo externo, como Banco de Dados e APIs externas.
  • Testes Funcionais Locais: Simulação completa em ambiente local para validar fluxos de negócios essenciais.

Junte-se a nós na jornada pelos testes locais do Sicredi no back-end, onde os testes são independentes de dados externos e destacam-se pela rapidez e eficiência, descobrindo através de exemplos práticos como cada nível de teste contribui para a excelência do nosso software.

Pirâmide de Testes Locais

A seguir você pode visualizar a pirâmide de testes locais:

API de Pedidos: Um Estudo de Caso Prático

Para exemplificar as camadas da pirâmide de testes, criamos a API de Pedidos, um microserviço que demonstra como aplicamos testes unitários, de integração local e funcionais locais na prática. Veja a arquitetura do serviço abaixo:

Arquitetura da API de Pedidos

Para explorar o código e mergulhar mais profundamente na estrutura e nos testes implementados, acesse a API de Pedidos no GitHub. O exemplo utiliza Java com Spring Boot, mas os conceitos podem ser aplicados em qualquer tecnologia de back-end.

Testes Unitário: O Alicerce do Desenvolvimento

Os testes unitários são vitais para o desenvolvimento de software, focando na validação de unidades individuais de código para detectar erros cedo, otimizar o design e minimizar a complexidade. Eles se destacam pela rapidez, com execuções que chegam a milhares em poucos segundos, e pela independência, evitando o uso de infraestrutura externa através de “mocks”.

Nosso exemplo no GitHub, a classe PedidoService, ilustra essa prática ao testar cenários de sucesso e falha, oferecendo feedback imediato sobre a integridade do código. Abaixo é possível ver a classe PedidoService destacada no diagrama da API de Pedidos:

  @Test
void deveriaCriarUmPedidoQuandoTodosItensPossuemEstoque() {
Long pedidoId = 1L;

when(pedidoRepository.save(PEDIDO)).thenReturn(pedidoId);
when(estoqueClient.get(PRODUTO_ID_1))
.thenReturn(new EstoqueResponse(PRODUTO_ID_1, 10L));
when(estoqueClient.get(PRODUTO_ID_2))
.thenReturn(new EstoqueResponse(PRODUTO_ID_2, 10L));

pedidoService.cria(PEDIDO);

verify(pedidoCriadoProducer).sendPedidoCriadoMessage(pedidoId);
verify(pedidoRepository).save(PEDIDO);
}

Veja os testes completos no GitHub.

Esta abordagem garante que nossos sistemas sejam robustos e confiáveis, mantendo o desenvolvimento ágil e focado na qualidade.

Testes de Integração Local: Sinergia e Confiança entre os sistemas

Os Testes de Integração Local são cruciais para assegurar que diferentes partes do nosso sistema não apenas funcionem isoladamente, mas também em harmonia com outros sistemas. Eles validam a interação do nosso código com elementos críticos como bancos de dados, sistemas de mensageria, APIs externas e frameworks, eliminando surpresas desagradáveis em produção.

Exemplos Práticos:

  • Testes com Client HTTP (APIs Externas): Utilizamos simulações de chamadas a APIs externas para verificar nossa integração e comportamento esperado. Veja o exemplo de como testamos o EstoqueClient, garantindo que as respostas das APIs sejam manuseadas corretamente.
Diagrama destacando a classe sendo testada (EstoqueClient)
@Test
void deveriaBuscarAQuantidadeDaApiDeEstoque() {
stubFor(get(urlEqualTo("/produtos/1"))
.willReturn(aResponse().withBodyFile("estoque_produto_1_response.json")
.withHeader("Content-Type", "application/json")));

EstoqueResponse response = estoqueClient.get("1");

assertThat(response).isEqualTo(new EstoqueResponse("1", 20L));
}
  • Testes de Controller: Validamos que nossos endpoints RESTful entreguem as respostas corretas aos usuários. Confira o teste, onde verificamos status e conteúdo da resposta, assegurando uma experiência de usuário impecável.
Diagrama destacando a classe sendo testada (PedidoController)
@Test
void criaPedido() throws Exception {
String pedidoJson = new ClassPathResource("json/pedido.json")
.getContentAsString(Charset.defaultCharset());

mvc.perform(post("/pedidos")
.contentType(MediaType.APPLICATION_JSON)
.content(pedidoJson))
.andExpect(status().isCreated());

verify(pedidoService).cria(PEDIDO);
}
  • Testes com Kafka: Garantimos que as mensagens sejam produzidas e consumidas conforme o esperado em nossos tópicos. Neste exemplo, testamos a produção de mensagens, crucial para a comunicação assíncrona eficaz.
Diagrama destacando a classe sendo testada (PedidoProducer)
@Test
void deveriaEnviarUmEventoDePedidoCriado() throws JSONException {
Consumer<String, String> consumer =
kafkaTestUtils.createConsumer(TOPIC_NAME);

Long pedidoId = 1L;
pedidoCriadoProducer.sendPedidoCriadoMessage(pedidoId);

ConsumerRecord<String, String> record =
kafkaTestUtils.getLastRecord(consumer, TOPIC_NAME);

var jsonEvent = new JSONObject(record.value());
assertThat(jsonEvent.getLong("pedidoId")).isEqualTo(pedidoId);
}
  • Testes com Banco de Dados: Verificamos a integridade das operações de banco de dados, como buscas e inserções. Aqui, demonstramos como nossos repositórios interagem de maneira confiável com o banco de dados.
Diagrama destacando as classes sendo testadas (PedidoRepository e PedidoJPARepository)
@Test
void deveriaSalvarUmPedido() {
Long id = pedidoRepository.save(PEDIDO);

PedidoEntity pedidoEntity = pedidoJpaRepository.findById(id).orElseThrow();

assertThat(pedidoEntity.getEnderecoEntrega()).isEqualTo(ENDERECO);
assertThat(pedidoEntity.getItens()).hasSize(2);

assertThat(pedidoEntity)
.usingRecursiveComparison()
.ignoringFields("id", "itens.id", "itens.pedido")
.isEqualTo(pedidoMapper.pedidoToPedidoEntity(PEDIDO));
}

Cada um desses testes nos ajuda a construir um microserviço mais resiliente, promovendo a confiança na qualidade e na integração de nossos sistemas.

Teste Funcional Local: Validando o Coração da Aplicação

Os testes funcionais locais são a culminação do nosso processo de testes, focados em assegurar que a aplicação, como um todo, entrega a funcionalidade esperada. Elevam a confiança no software ao simular o ambiente de produção e testar os fluxos de negócios end-to-end — como, por exemplo, o ciclo completo de uma compra em um e-commerce.

Realizados após a validação unitária e de integração, esses testes trazem à tona a aplicação e suas dependências em um ambiente local. Isso nos permite um controle abrangente e a capacidade de refinar os cenários mais críticos para o sucesso do negócio, garantindo que cada funcionalidade atenda às expectativas dos usuários finais.

Nosso teste end-to-end de um pedido exemplifica essa prática ao detalhar a jornada de compra, do início ao fim, validando a integração perfeita e a experiência do usuário.

Abaixo é possível ver as classes afetadas por esse teste, o que deixa claro que todas classes estão sendo utilizadas:

@Test
void deveriaCriarUmPedidoEEnviarOEvento() throws Exception {
String pedidoJson = new ClassPathResource("json/pedido.json")
.getContentAsString(Charset.defaultCharset());

Consumer<String, String> consumer =
kafkaTestUtils.createConsumer(TOPIC_NAME);

stubFor(get(urlEqualTo("/produtos/1"))
.willReturn(aResponse().withBodyFile("estoque_produto_1_response.json")
.withHeader("Content-Type", "application/json")));

stubFor(get(urlEqualTo("/produtos/2"))
.willReturn(aResponse().withBodyFile("estoque_produto_2_response.json")
.withHeader("Content-Type", "application/json")));

mvc.perform(post("/pedidos")
.contentType(MediaType.APPLICATION_JSON)
.content(pedidoJson))
.andExpect(status().isCreated());

ConsumerRecord<String, String> record =
kafkaTestUtils.getLastRecord(consumer, TOPIC_NAME);

var jsonEvent = new JSONObject(record.value());
assertThat(jsonEvent.getLong("pedidoId")).isNotNull();
}

Este enfoque garante que a aplicação não só funciona em teoria, mas também na prática, em condições reais de uso.

Conclusão e Olhando para o Futuro

Com este artigo, concluímos nossa visão geral sobre a pirâmide de testes locais no Sicredi, abarcando desde a importância dos testes unitários até a relevância dos testes funcionais locais. Ilustramos como cada nível da pirâmide reforça nosso compromisso com a qualidade e a confiança no software, pilares fundamentais para o desenvolvimento ágil e eficiente.

Sua opinião é crucial para nós! Deixe seus comentários, dúvidas ou feedbacks, e compartilhe este artigo se ele foi útil para você. Fique ligado para mais insights e práticas recomendadas que ajudarão a elevar a qualidade do seu desenvolvimento de software.

Até a próxima!

#engenharia-de-software

--

--