DEV Community

loading...
Cover image for Quarkus e TestContainers o Pikachu dos ambientes de testes

Quarkus e TestContainers o Pikachu dos ambientes de testes

lucasscharf profile image Aleatório ・6 min read

Me considero uma pessoa que pensa no bem comum e em ajudar o próximo. Por exemplo, caso eu encontrasse um gênio que me desse direito a um único desejo, eu escolheria que os pokémons existissem no mundo real. Porém, se ele me desse direito a um segundo desejo, eu escolheria que todos os desenvolvedores soubessem usar os test containers.

O que é?

O Testcontainers é uma biblioteca Java que permite subir instâncias docker durantes o ciclo de testes. Com essas instâncias, é possível ter acesso ao banco de dados, sistema de mensageria, cache, etc...

Mas é importante ter atenção pra algo muito importante:
É NECESSÁRIO TER O DOCKER INSTALADO NA MÁQUINA QUE FOR RODAR OS TESTES.

O que faremos?

O teste de hoje será um pouco mais elaborado. Vamos usar o que nós já aprendemos sobre banco de dados e mensageria. Vamos criar uma classe que cria um Pokémon e armazena ele no c̶i̶n̶t̶o̶ ̶d̶e̶ ̶p̶o̶k̶e̶b̶ó̶l̶a̶s̶ banco de dados.
Após isso, será disparado um evento ao kafka onde um treinador rival irá batalhar e nós recuperaremos o nosso pokémon para a batalha.

Alt Text

Hora da ação

Primeiramente vamos configurar nosso ambiente com as dependências necessárias. Por isso, é necessário adicionar a seguinte configuração no nosso pom.xml. Essas extensões configuram o conector de mensagens Kafka, a comunicação com banco de dados (via Panache) e o Test Containers.

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-jdbc-postgresql</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-hibernate-orm-panache</artifactId>
    </dependency>
    <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>testcontainers</artifactId>
      <version>1.15.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>kafka</artifactId>
      <version>1.15.1</version>
      <scope>test</scope>
    </dependency>
Enter fullscreen mode Exit fullscreen mode

Segundamente, vamos criar nossa classe de Pokémon. Ela é bem simples, tem o nome e o número dele. Isso é feito através da classe abaixo:

package org.acme;

import javax.persistence.Entity;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Pokemon extends PanacheEntity{
  private int codigo;
  private String nome;

  public Pokemon() {
  }

  public Pokemon(int codigo, String nome) {
    this.codigo = codigo;
    this.nome = nome;
  }

  public int getCodigo() {
    return codigo;
  }

  public String getNome() {
    return nome;
  }

}

Enter fullscreen mode Exit fullscreen mode

Agora vamos fazer nosso gerador de batalhas. Ao receber uma mensagem de que um treinador rival quer entrar em batalha (recebendo o nome do treinador), ele gravará no log o nome do treinador, pegará o primeiro pokémon disponível no banco de dados e gravará no log o nome desse pokémon. O código da batalha está baixo.

package org.acme;

import javax.enterprise.context.ApplicationScoped;
import javax.transaction.Transactional;

import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.smallrye.reactive.messaging.annotations.Blocking;

@ApplicationScoped
public class GeradorDeBatalha {
  private static final int PRIMEIRO_ELEMENTO = 0;
  Logger logger = LoggerFactory.getLogger(getClass());

  @Incoming("novo-combate")
  @Blocking
  @Transactional
  public void iniciarCombate(String nomeDoTreinador) {
    logger.info("O treinador [{}] quer iniciar um combate", nomeDoTreinador);
    Pokemon pokemon = (Pokemon) Pokemon.listAll().get(PRIMEIRO_ELEMENTO);
    logger.info("[{}], eu escolho você!", pokemon.getNome());
  }
}
Enter fullscreen mode Exit fullscreen mode

Vamos fazer o nosso teste. Por fins didáticos, não fazemos nenhum tipo de asserção sobre ele, apenas olhando
o logo impresso. Como são threads diferentes que irão fazer o processamento (uma thread vai emitir a mensagem de batalha e a outra vai consumir), nós precisamos dar um tempo na nossa thread de teste para que a outra faça o seu trabalho.

O código para testes é esse aqui:

package org.acme;

import javax.inject.Inject;
import javax.transaction.Transactional;

import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
@QuarkusTestResource(Dependencias.class)
public class BatalhaTest {
  @Channel("iniciar-combate")
  @Inject
  Emitter<String> emissor;

  @BeforeEach
  @Transactional
  public void cadastrarPokemon() {
    Pokemon pokemon = new Pokemon(25, "Pikachu");
    pokemon.persist();
  }

  @Test
  public void combateSimples() throws Exception {
    emissor.send("Gary Carvalho");
    Thread.sleep(1_000);
  }
}
Enter fullscreen mode Exit fullscreen mode

Feito isso, vamos configurar o application.properties com o banco de dados, um consumidor e um emissor de mensagens de combate (esse emissor será usado só para os testes).

quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = ash
quarkus.datasource.password = pallet
quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/pokemon

quarkus.hibernate-orm.database.generation = update


mp.messaging.incoming.novo-combate.connector=smallrye-kafka
mp.messaging.incoming.novo-combate.topic=novo-combate
mp.messaging.incoming.novo-combate.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer

%test.mp.messaging.outgoing.iniciar-combate.connector=smallrye-kafka
%test.mp.messaging.outgoing.iniciar-combate.topic=novo-combate
%test.mp.messaging.outgoing.iniciar-combate.value.serializer=org.apache.kafka.common.serialization.StringSerializer
Enter fullscreen mode Exit fullscreen mode

Caso tenha ficado alguma dúvida sobre o funcionamento da parte de configuração, tem dois posts topzeiras aqui e aqui.

Tudo certo né? Agora é só mandar o maven testar e podemos ser felizes. Certo? Errado!
Alt Text

Como não existe nem o banco de dados rodando e nem o kafka, a aplicação nem subirá direito.
Alt Text

Para que a aplicação suba corretamente, é necessário fazer as dependências rodarem e é aqui que os tests containers entram.

Nós já configuramos eles, agora basta criarmos os containers para testes. Isso pode ser feito através da classe abaixo.

package org.acme;

import java.util.HashMap;
import java.util.Map;

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.utility.DockerImageName;

import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

public class Dependencias implements QuarkusTestResourceLifecycleManager {
  private final KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka")); //0

  private static final DockerImageName POSTGRES_IMAGE = DockerImageName.parse("postgres:12-alpine"); // 1
  private GenericContainer<?> postgresContainer = new GenericContainer<>(POSTGRES_IMAGE); //2

  public Map<String, String> start() {
    postgresContainer.withEnv("POSTGRES_USER", "usuario")// 3
        .withEnv("POSTGRES_PASSWORD", "senha")
        .withExposedPorts(5432)// 4
        .withEnv("POSTGRES_DB", "bancoDeDados"); // 5
    postgresContainer.start();//6

    kafka.start();//7

    Map<String, String> properties = new HashMap<>();
    properties.put("quarkus.datasource.username", "usuario"); //8
    properties.put("quarkus.datasource.password", "senha");
    properties.put("quarkus.datasource.jdbc.url",//
        "jdbc:postgresql://"+ postgresContainer.getHost() +":" + postgresContainer.getFirstMappedPort() + "/bancoDeDados");//9
    properties.put("kafka.bootstrap.servers", kafka.getBootstrapServers());//10

    return properties;
  }

  public void stop() {
  }
}
Enter fullscreen mode Exit fullscreen mode

Essa é a classe que cria os containers. Vamos entender o que ela faz?

  • 0 - Container kafka que já tem tudo pré-configurado (ter um container desses é muito útil)
  • 1 - Imagem docker para a utilização de um container genérico
  • 2 - Container genérico, onde vamos configurar os valores para fazer nossos testes
  • 3 - Configuração do usuário
  • 4 - Configuração da senha
  • 5 - Configuração do banco (notem que é uma boa prática usar valores diferentes dos que estão no application.properties)
  • 6 - Expondo a porta interna 5432 (o testContainers poderá mapear essa porta interna para alguma outra porta externa)
  • 7 - Iniciando o container postgres
  • 8 - Iniciando o container kafka
  • 9 - Atualizando as configurações que serão utilizadas durante o teste
  • 10 - Na falta de um container específico, as configurações ficam assim e isso pode gerar erros
  • 11 - Utilização de propriedade com container específico
  • 12 - Retornando as propriedades

E nós só precisamos de mais um passo que é informar para a nossa classe de teste de que é para ele utilizar o nosso recurso de teste. Isso é feito colocando a anotação de classe
@QuarkusTestResource(Dependencias.class).

Agora sim é só rodar a nossa aplicação com o mvn test e correr pro abraço.

2021-02-08 12:08:14,720 INFO  [org.acm.GeradorDeBatalha_Subclass] (vert.x-worker-thread-1) O treinador [Gary Carvalho] quer iniciar um combate
2021-02-08 12:08:14,811 INFO  [org.acm.GeradorDeBatalha_Subclass] (vert.x-worker-thread-1) [Pikachu], eu escolho você!
Enter fullscreen mode Exit fullscreen mode

Considerações

Fiz um resumão super resumido sobre os tests containers e Quarkus. Tem bastante coisa pra falar e espero fazer mais posts sobre isso. Outra coisa interessante é que notei que boa parte dos meus textos são sobre testes e estou feliz com isso. Um mundo com mais testes, é um mundo mais feliz.

Ah, e o código de hoje pode ser encontrado no github.

Discussion (1)

pic
Editor guide
Collapse
dearrudam profile image
Maximillian Arruda

Belo post!!! Valeu por compartilhar!!! Bora praticar!!! Heheh