Pular para conteúdo

Integração

3. Testes de integração

Os testes de integração complementam os testes unitários ao fornecer uma visão mais abrangente e holística do sistema como um todo. Juntos, esses dois tipos de testes desempenham papéis distintos e se complementam de várias maneiras.

3.1. O que deve ser testado?

Kent Beck destaca que nos testes de integração, o foco deve estar na colaboração entre as diferentes partes do sistema para garantir que elas se integrem corretamente e funcionem em conjunto. A ideia é verificar se os componentes ou módulos do sistema operam harmoniosamente quando conectados.

Em termos práticos, nos testes de integração, deve-se verificar:

  1. Fluxo Completo de Funcionalidades:

    • Testar o fluxo completo de funcionalidades, desde a entrada do usuário até a saída, considerando todos os componentes envolvidos.
  2. Colaboração entre Componentes:

    • Garantir que os diferentes componentes ou módulos interajam conforme o esperado. Isso inclui a troca de dados, chamadas de métodos, e qualquer outra forma de comunicação entre partes do sistema.
  3. Integração com Dependências Externas:

    • Verificar se o sistema se integra corretamente com serviços externos, APIs de terceiros, bancos de dados, ou qualquer outra dependência externa.
  4. Manipulação de Dados Compartilhados:

    • Avaliar como o sistema manipula dados compartilhados entre diferentes partes. Isso é especialmente importante em ambientes distribuídos ou arquiteturas de microserviços.
  5. Configuração e Ambiente de Produção:

    • Verificar se as configurações do ambiente de produção estão corretamente integradas e se o sistema se comporta como esperado em condições reais.
  6. Tratamento de Erros e Exceções:

    • Avaliar como o sistema lida com erros e exceções durante a execução normal e durante a integração de diferentes componentes.
  7. Desempenho e Escalabilidade:

    • Testar o desempenho e a escalabilidade do sistema quando diferentes partes são integradas. Isso é especialmente importante para sistemas que precisam lidar com um grande volume de transações.
  8. Segurança:

    • Verificar se as interfaces de integração estão protegidas contra ameaças de segurança e se os mecanismos de autenticação e autorização estão funcionando corretamente.

Os testes de integração visam garantir que as partes do sistema operem harmoniosamente juntas, fornecendo uma visão mais abrangente e realista do funcionamento do software em um ambiente integrado. Este enfoque colaborativo é fundamental para assegurar a qualidade do sistema como um todo.

3.2. Como deve ser testado?

A ideia do teste de integração é realizar, num ambiente controlado e isolado, uma interação real com uma parte do sistema, trazendo cenários de sucesso e erro.

Se não há uma execução real de uma ou mais partes so software, não é um teste de integração

Vamos criar um exemplo usando Spring Boot para verificar se um controlador Rest recebe dados e os salva no banco de dados. Neste exemplo, utilizaremos um banco de dados embarcado H2 para simplificar.

  1. Adicionar Dependências no arquivo pom.xml (Maven): Certifique-se de ter as dependências necessárias para o Spring Boot, Spring Data JPA e o banco de dados H2.
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- H2 Database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Test Dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
  1. Configurar a aplicação para usar o banco de dados H2: Configure o arquivo application.properties ou application.yml para usar o banco de dados H2.
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
  1. Criar a Classe de Entidade e Repositório: Vamos criar uma classe de entidade Item e um repositório ItemRepository para representar os dados que serão salvos no banco.
// Item.java
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Item {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String nome;

    // getters, setters, construtores
}
// ItemRepository.java
import org.springframework.data.jpa.repository.JpaRepository;

public interface ItemRepository extends JpaRepository<Item, Long> {
}
  1. Criar o Controlador Rest (RestController): Vamos criar um controlador simples que recebe dados por meio de uma solicitação REST e os salva no banco de dados usando o ItemRepository.
// ItemController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ItemController {

    private final ItemRepository itemRepository;

    @Autowired
    public ItemController(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }

    @PostMapping("/items")
    public void criarItem(@RequestBody Item item) {
        itemRepository.save(item);
    }
}
  1. Criar o Teste de Integração: Agora, criaremos um teste de integração usando Spring Boot para verificar se o controlador salva corretamente os itens no banco de dados.
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class ItemIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @LocalServerPort
    private int port;

    @Test
    public void testarSalvarItem() {
        // Configurar a URL do controlador
        String url = "http://localhost:" + port + "/items";

        // Criar um item de exemplo
        Item item = new Item();
        item.setNome("Item de Teste");

        // Enviar a solicitação POST para salvar o item
        ResponseEntity<Void> responseEntity = restTemplate.exchange(
            url, HttpMethod.POST, new HttpEntity<>(item), Void.class
        );

        // Verificar se o item foi salvo corretamente
        assertThat(responseEntity.getStatusCodeValue()).isEqualTo(200); // ou 201, dependendo da configuração
        assertThat(itemRepository.count()).isEqualTo(1);
    }
}

Note como é simples criar um teste que executa uma ação real no sitema. Entretanto, este teste não está parecido com um recnário de produção. Em um cenário produtivo um banco de dados não é criado a partida da aplicação e pode ser mudado a qualquer momento, trazendo falsos negativos por exemplo.

Um teste com este formato é viciado, pois suas dependencias estão acopladas ao cenário proposto.

3.2.1. Isolando as dependencias

Uma característica forte do teste de integração é que ele necessita de um ambiente para ser executado. Ora, se o objetivo do teste de integraçao é simular uma execução real de uma parte om um conjunto de partes do sistema, é natural que ele seja executado num ambiente concreto.

Configurar ambientes não é tarefa trivial, pode envolver configurações de rede, sistemas externos, bancos de dado, mensageria, dentre outras dependencias alheias ao software desenvolvido.

Quando a necessidade envolve ambiente, o docker tem propriedade para solucionar problemas dessa natureza e, no universo dos testes, o produto Testcontainer se tornou peça chave para a eficácia.

3.2.1.1 O Testcontainer

O Testcontainers é uma biblioteca Java que facilita a execução de contêineres Docker durante os testes automatizados.

Principais características e benefícios do Testcontainers:

  1. Gerenciamento Simples de Contêineres Docker:

    • O Testcontainers simplifica a inicialização, execução e desligamento de contêineres Docker necessários para testes de integração.
  2. Suporte a Diversos Bancos de Dados e Serviços:

    • Oferece suporte a uma variedade de contêineres pré-configurados para bancos de dados (MySQL, PostgreSQL, Oracle, etc.), sistemas de mensagens (Kafka, RabbitMQ), e outros serviços comumente usados em aplicações.
  3. Integração com Frameworks de Teste:

    • Pode ser integrado facilmente a frameworks de teste populares, como JUnit e TestNG, facilitando a incorporação de contêineres Docker aos cenários de teste.
  4. Isolamento de Ambiente:

    • Cria ambientes isolados para cada teste, evitando interferências entre diferentes execuções de testes.
  5. Controle Dinâmico:

    • Permite a criação dinâmica de contêineres com configurações específicas para cenários de teste, garantindo flexibilidade.
  6. Manutenção Simples:

    • Elimina a necessidade de configurar ambientes de teste complexos e mantém a consistência nos ambientes de desenvolvimento e produção.
  7. Facilita Testes de Integração Reais:

    • Permite a execução de testes em ambientes que se assemelham mais aos ambientes de produção, melhorando a confiabilidade dos testes de integração.
  8. Facilita Testes Locais e em Ambientes de Integração Contínua:

    • Pode ser utilizado localmente pelos desenvolvedores durante o desenvolvimento e integrado a pipelines de integração contínua para garantir a consistência nos ambientes de teste.

3.2.1.2. Como configurar o Testcontainer

Para utilizar o Testcontainers em projetos Java, é necessário realizar algumas etapas:

  1. Adicionar Dependência:
    • Adicione a dependência do Testcontainers em seu arquivo de configuração do projeto (por exemplo, arquivo pom.xml se estiver usando Maven, ou build.gradle se estiver usando Gradle).

Exemplo com Maven:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.16.3</version> <!-- Verifique a versão mais recente no repositório Maven -->
    <scope>test</scope>
</dependency>

Exemplo com Gradle:

testImplementation 'org.testcontainers:testcontainers:1.16.3' // Verifique a versão mais recente no repositório Maven

  1. Configuração do Ambiente:

    • Certifique-se de ter o Docker instalado em sua máquina, pois o Testcontainers usa o Docker para criar e gerenciar contêineres.
  2. Uso em Testes:

    • Integre o Testcontainers em seus testes. Isso pode ser feito através de anotações ou configurações específicas nos testes.

Exemplo de uso em um teste JUnit 5:

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;

public class MeuTeste {

    @Test
    public void meuTesteComPostgres() {
        try (PostgreSQLContainer<?> container = new PostgreSQLContainer<>()) {
            container.start();

            // Aqui você pode acessar as informações do contêiner, como URL do banco de dados, usuário, senha, etc.
            String jdbcUrl = container.getJdbcUrl();
            String username = container.getUsername();
            String password = container.getPassword();

            // Restante do código do teste que utiliza o contêiner PostgreSQL
        }
    }
}

  1. Configurações Adicionais (Opcional):
    • Dependendo dos casos de uso específicos, você pode precisar configurar alguns detalhes adicionais, como expor portas, configurar variáveis de ambiente no contêiner, etc. Isso pode ser feito através das APIs do Testcontainers.

3.2.1.3. Utilização com Spring Boot

Integrar o Testcontainers com o Spring Boot é simples, uma vez adicionada a dependencia maven ou gradle basta:

1**Configurar um Contêiner no Teste:** - No seu teste de integração, configure um contêiner usando a classe apropriada do Testcontainers para o serviço que você está testando.

Exemplo de teste de integração com um banco de dados PostgreSQL:

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@SpringBootTest
@Testcontainers
public class MeuTesteIntegracao {

    @Container
    private static final PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>();

    @Test
    public void meuTesteComPostgres() {
        // As configurações do banco de dados agora podem ser acessadas através de postgresContainer.getJdbcUrl(), etc.
        // O Spring Boot detectará automaticamente a porta do contêiner e usará as configurações para a conexão com o banco de dados.
        // O contêiner será iniciado antes dos testes e encerrado automaticamente após a conclusão dos testes.
    }
}

2**Configurar Propriedades do Spring Boot:** - No seu arquivo application.properties ou application.yml de teste, configure as propriedades do Spring Boot para usar as informações do contêiner.

Exemplo (application-test.properties):

spring.datasource.url=jdbc:postgresql://${postgresContainer.getContainerIpAddress()}:${postgresContainer.getMappedPort(5432)}/mydatabase
spring.datasource.username=myuser
spring.datasource.password=mypassword

Com esses passos, é possível integrar o Testcontainers com o Spring Boot para testar componentes que dependem de serviços externos, garantindo um ambiente de teste mais próximo do ambiente de real.

3.2.1.4. Equilibrio entre o ambiente e os cenários de teste

Mesmo que o objetivo seja estar o mais próximo possível do da produção, é dispensável, para a maioria dos cenários de teste, que o ambiente esteja completo.

É inviavel um cenário de testes que testa tudo.

Assim, o ideal é que cada dependencia esteja isolada na sua configuração, de forma que utilizando o test container, por exemplo, cada dependencia deve ser executada em separado, no seu proprio container conforme a necessidade do teste.

3.2.2 Desenhando o teste

Um cenário apropriado para teste de integração é, por exemplo, uma comunicação entre microserviços.

Vamos considerar um exemplo onde um sistema de e-commerce precisa validar se um pedido é elegível para frete grátis. Essa regra de negócio depende da quantidade total de produtos no pedido e pode envolver comunicação com diferentes microserviços, como o serviço de pedidos, o serviço de produtos e o serviço de clientes.

  1. Implementação do Serviço de Pedidos (PedidoService):

    @Service
    public class PedidoService {
    
        private final ProdutoService produtoService;
        private final ClienteService clienteService;
    
        @Autowired
        public PedidoService(ProdutoService produtoService, ClienteService clienteService) {
            this.produtoService = produtoService;
            this.clienteService = clienteService;
        }
    
        public DetalhesPedido obterDetalhesPedido(Long pedidoId) {
            // Lógica para obter detalhes de um pedido
            // Inclui a necessidade de chamar o serviço de produtos e clientes para obter informações associadas ao pedido
            Produto produto = produtoService.obterProdutoPorId(produtoId);
            Cliente cliente = clienteService.obterClientePorId(clienteId);
            // Lógica para processar os detalhes do pedido com base nas informações do produto e cliente
            // ...
    
            return detalhesPedido;
        }
    }
    

  2. Regra de Negócio: Verificação de Elegibilidade para Frete Grátis:

    @Service
    public class FreteGratisService {
    
        public boolean verificarElegibilidadeFreteGratis(DetalhesPedido detalhesPedido) {
            // Lógica para verificar se o pedido é elegível para frete grátis
            // Dependendo da quantidade de produtos no pedido, pode ser necessário chamar outros microserviços
            // ...
    
            return detalhesPedido.getQuantidadeTotal() >= 3; // Exemplo simplificado, considerando frete grátis para pedidos com 3 ou mais produtos
        }
    }
    

  3. Teste de Integração usando Testcontainers:

    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    @SpringBootTest
    @Testcontainers
    public class ElegibilidadeFreteGratisIntegrationTest {
    
        @Container
        private static final PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>();
    
        @Autowired
        private PedidoService pedidoService;
    
        @Autowired
        private FreteGratisService freteGratisService;
    
        @Test
        public void testarElegibilidadeFreteGratis() {
            // Configurar dados de teste no banco de dados (usando Testcontainers para criar o banco de dados PostgreSQL)
            // ...
    
            // Chamar o serviço de pedidos para obter detalhes do pedido
            DetalhesPedido detalhesPedido = pedidoService.obterDetalhesPedido(123L);
    
            // Verificar se a regra de negócio de elegibilidade para frete grátis está correta
            assertTrue(freteGratisService.verificarElegibilidadeFreteGratis(detalhesPedido));
        }
    }
    

Note que neste exemplo foi possível validar o comportamento real da interação entre 3 partes do sistema.

Alguns desenvolvedores optam por utilizar Mocks em situações como estas, mas, como já descrito na seção de testes unitários, os mocks auxiliam no isolamento do cenário de testes. Neste caso simular o comportamento do banco de dados ou do serviço de pedidos esconderia possíveis comportamentos inesperados ao executar o método obterDetalhesPedido.

Note também que, apesar de haver esforço para simular o ambiente do sistema, o foco ainda é em cenários concisos e replicáveis.

É importante ressaltar que os testes de integração devem ser tão estáveis e independentes de implementação quanto os testes unitários.