Teste de integração com TestContainer

Teste a integração da persistência de sua aplicação java com uma instância real do PostgreSQL usando TestContainer Criar teste...

Eduardo Costa

View posts by Eduardo Costa
Desenvolvedor Java apaixonado por tecnologia. Gosta de ler, escrever e principalmente ajudar outros desenvolvedores a construir e implantar aplicações cloud-native que sejam seguras, escalaveis e resiliente para que possamos juntos crescer em nossas carreiras se divertindo tecnologias inovadoras.
Data de publicação: 19/02/2020

Teste a integração da persistência de sua aplicação java com uma instância real do PostgreSQL usando TestContainer

Criar teste de integração eficiente é um desafio comum durante a construção de microsserviços. Garantir a similaridade do ambiente de desenvolvimento com o ambiente de produção costuma ser trabalhoso. Principalmente se as ferramentas e serviços usados exigirem instalação e configuração manual. Neste caso acabamos por adotar ferramentas mais leves para desenvolver e testar aumentando a disparidade entre os ambientes.

O guia de boas práticas Twelve Factor Apps prega a similaridade entre os ambientes como um dos fatores de sucesso para construção de aplicações cloud-native. Desenvolver e testar em ambientes cuja disparidade seja baixa permite criar aplicações mais resilientes trazendo segurança e velocidade nas implantações.

Neste artigo veremos como criar um teste de integração usando a biblioteca Testcontainer. Este teste irá inicializar um container do PostgreSQL e executar testes de leitura e persistência nesta instância real de banco de dados. Usaremos como exemplo um projeto simples que possui uma classe DAO (Data Access Object) responsável pela execução de diversas queries SQL nesta instância real.

O que é Testcontainer ?

Testcontainer é uma biblioteca java que dá suporte a testes unitários. Ela fornece instâncias leves e descartáveis de banco de dados ou qualquer aplicação que possa rodar em um container Docker. Ela facilita a criação de testes de integração com o uso de containers sem a necessidade de criar configurações complexas.

Pré requisitos

Para executar o código de exemplo disponibilizado no github, além da ferramenta Git e uma IDE, você vai precisar ter o Docker instalado e executar com sucesso o seguinte comando:

docker info

Caso você use o sistema operacional Linux, observe que não foi usado o prefixo sudo ao comando. Deve ser possível executar este comando sem a necessidade do prefixo sudo. Porque durante a execução do teste de integração, o docker será acionado pela biblioteca para baixar a imagem e criar o container.

Para executar os comandos do docker sem a necessidade do prefixo sudo, seu usuário deve ser incluído no grupo de usuários docker com o seguinte comando:

sudo usermod -aG docker $USER

Após a execução deste comando é necessário logar novamente na sessão para que a alteração tenha efeito.

Projeto com exemplo de teste de integração

Este projeto possui um teste de integração que inicializa um container PostgreSQL, prepara o estado inicial do banco de dados com um script SQL, realiza a execução dos testes e automaticamente destrói o container ao terminar.

Para ver o código clone o repositório do github disponibilizado com o seguinte comando:

git clone https://github.com/educostadev/poc-testcontainers.git

Importe o projeto clonado como um novo projeto Maven e você estará pronto para inspecionar o código. Caso utilize a IDE Intellij, acesse este link para obter ajuda em como importar um projeto Maven.

O Testcontainer se integra com JUnit4 e JUnit5 por meio de dependência especificada no pom.xml. Abra o arquivo pom.xml do exemplo e observe a dependência responsável pelas funcionalidades do Testcontainer com JUnit5.


<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>junit-jupiter</artifactId>
   <version>1.12.3</version>
   <scope>test</scope>
</dependency>

Caso você use um banco de dados já suportado pelo Testcontainer, é necessário também adicionar a dependência disponibilizada pela biblioteca. Neste link você encontra uma lista dos banco de dados suportado nativamente pelo Testcontainer. Em nosso exemplo usamos uma instância do PostgreSQL, portanto segue abaixo a dependência:


<dependency>
   <groupId>org.testcontainers</groupId>
   <artifactId>postgresql</artifactId>
   <scope>test</scope>
</dependency>

Os outros arquivos java do projeto seguem as definições padrão de uma aplicação Spring Boot. No arquivo de configuração application.yml estão as definições de conexão com o banco de dados. Note que estas definições estão externalizadas em variáveis de ambiente com nome DB_URL, DB_USERNAME e DB_PASSWORD.

server:
  port: 8083
logging:
  level:
    ROOT: info
    org.hibernate.tool.hbm2ddl: debug
    org.hibernate.SQL: debug
    org.hibernate.type.descriptor.sql: trace
spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    driver-class-name: org.postgresql.Driver
    hikari:
      maximum-pool-size: 5
      connection-timeout: 20000
    initialization-mode: always
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    hibernate:
      ddl-auto: create

A classe DummyDao usa a biblioteca jdbcTemplate para executar queries SQL no banco de dados. Esta é a classe que terá suas queries testadas em um container PostgreSQL.


@Repository
public class DummyDao {
  @Autowired
  private JdbcTemplate template;
  public Dummy findDummyById(int id) {
    return template
        .queryForObject("SELECT * FROM dummy_test where id=?", new Object[]{id},
            new DummyRowMapper());
  }

  public List readAll() {
    return template.query("SELECT id,name_value FROM dummy_test", new DummyRowMapper());
  }

  public int save(Dummy dummy) {
    return template.update("INSERT INTO dummy_test (id,name_value) VALUES (?,?) ",
        new Object[]{dummy.getId(), dummy.getName()});
  }

  static class DummyRowMapper implements RowMapper {
    @Override
    public Dummy mapRow(ResultSet rs, int rowNumber) throws SQLException {
      return new Dummy(rs.getInt("id"), rs.getString("name_value"));
    }
  }
}

Observe o teste abaixo, ele injeta uma instância da classe DummyDao. Os métodos subsequentes realiza um teste de inserção e leitura de dados. Tratando-se de um teste de integração, não é realizado mock no comportamento da classe DummyDao o que significa que espera-se uma conexão real com o banco de dados.

No início da classe é obtido de forma estática uma instância do container PostgreSQL. Apesar da instância não ser usada no corpo do teste é através dela que o container é criado e iniciado.

A inicialização estática deste atributo permite que o mesmo container seja aproveitado durante a execução de vários testes evitando que ele seja destruído e reconstruído a cada teste. A anotação @Testcontainer e @Container cuida do ciclo de vida deste container permitindo que ele seja iniciado uma única vez e descartado ao final dos testes.

@SpringBootTest
@ContextConfiguration
@Testcontainers
public class DummyDaoTest {
 
@Container
public static PostgreSQLContainer postgreSQLContainer = CustomPostgresContainer.getInstance();
 
@Autowired
DummyDao dao;
 
@Test
void injectedComponentsAreNotNull() {
  assertNotNull(dao);
}

@Test
void save_new_value() {
  Dummy value = new Dummy(1000, "dummy");
  int affectedRows = dao.save(value);
  assertEquals(1, affectedRows);
}

@Test
void read_all() {
  assertThat(dao.readAll()).isNotEmpty();
}

}

A classe CustomPostgresContainer é a responsável por fazer o download da imagem, criar e iniciar o container PostgreSQL na versão alpine. Poderia ser usada qualquer versão disponível no Docker Hub ou até mesmo uma imagem docker customizada. No caso de uma imagem customizada não disponível no docker hub, a imagem precisa existir na máquina onde o teste é executado.

Neste exemplo de demonstração, o container é criado passando um script sql para carga inicial dos dados. Entretanto, este é um passo opcional visto que os próprios testes unitários poderiam construir e inserir os dados necessários para realizar os testes.

O método sobrescrito start() desempenha um papel importante nesta infraestrutura de teste. É ele que obtém a url, usuário e senha de conexão com o banco de dados iniciado pelo container e escreve estes valores nas respectivas variáveis de ambiente.  Isso possibilita que os testes se conecte a esta instância real de banco de dados.


public class CustomPostgresContainer extends PostgreSQLContainer {

  private static final Logger logger = LoggerFactory.getLogger(CustomPostgresContainer.class);
  private static final String IMAGE_VERSION = "postgres:alpine";
  private static CustomPostgresContainer container;

  private CustomPostgresContainer() {
    super(IMAGE_VERSION);
  }

  public static CustomPostgresContainer getInstance() {
    if (container == null) {
      container = new CustomPostgresContainer();
    }
    return container;
  }

  @Override
  public void start() {
    super.start();
    logger.debug("POSTGRES INFO");
    logger.debug("DB_URL: " + container.getJdbcUrl());
    logger.debug("DB_USERNAME: " + container.getUsername());
    logger.debug("DB_PASSWORD: " + container.getPassword());
    System.setProperty("DB_URL", container.getJdbcUrl());
    System.setProperty("DB_USERNAME", container.getUsername());
    System.setProperty("DB_PASSWORD", container.getPassword());
  }

  @Override
  public void stop() {
    //do nothing, JVM handles shut down
  }
}

O fluxo de execução dos testes com o uso do TestContainer se resume a seguir:

Test Container
Fluxo de execução do Testcontainer
  • O primeiro teste da stack de testes unitário é executado;
  • O método getIntance() da classe CustomPostgresContainer é invocado;
    • A biblioteca do TestContainer verifica se a imagem especifica existe localmente;
    • Caso não exista é feito o download da imagem do docker hub;
  • É criado um container baseado na imagem baixada e iniciado o banco de dados;
  • Os testes utilizam o banco de dados disponibilizado;
  • Ao fim de todos os testes unitários o container é destruído;

Conclusão

Neste artigo vimos como a biblioteca Testcontainer torna fácil criar testes de integração com imagens Docker.  Permitindo diminuir a disparidade entre os ambientes de desenvolvimento e produção e trazer maior segurança na implantação. A biblioteca está em constante evolução e dá suporte nativo a diversos banco de dados além de possibilitar criar sua própria imagem customizada. Neste vídeoKevin Wittek uns dos mantenedor da biblioteca demonstra mais sobre o potencial do TestContainer.

Você já teve que fazer testes de integração que utilizem instâncias reais de algum tipo serviço ?  Quais ferramentas você usou e quais foram suas dificuldades ? Deixe um comentário abaixo.

 

Eduardo Costa

View posts by Eduardo Costa
Desenvolvedor Java apaixonado por tecnologia. Gosta de ler, escrever e principalmente ajudar outros desenvolvedores a construir e implantar aplicações cloud-native que sejam seguras, escalaveis e resiliente para que possamos juntos crescer em nossas carreiras se divertindo tecnologias inovadoras.

Deixe uma resposta

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

um − 1 =

Posts relacionados

  1. Sobre a Dextra

    Somos especialistas em desenvolvimento de software sob medida para negócios digitais. Pioneiros na adoção de metodologias de gestão ágil, combinamos processos de design, UX, novas tecnologias e visão de negócio, desenvolvendo soluções que criam oportunidades para nossos clientes.

  2. Categorias

Scroll to top