Aviso: Esse post ainda passará por uma revisão!
Testando Repositórios (De verdade!)
- Podemos testar repositórios de alguns jeitos, uma das maneiras é utilizar um banco em memória. Para SQL, h2 é fácil e rápido, podemos fazer as configs no banco usando um application-properties para os testes e validar tudo bonitinho normalmente 🙂. Outra alternativa seria utilizar TestContainers
- Conseguindo testar e subir repositórios e message brokers reais, conseguimos fazer testes de integração!
TestContainers
“Testcontainers é um framework de código aberto para fornecer instâncias descartáveis e leves de bancos de dados, message brokers, browsers ou praticamente qualquer coisa que possa ser executada em um container Docker.”
- Adicionando dependências:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<version>1.19.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.2</version>
<scope>test</scope>
</dependency>
A primeira dependência irá variar de acordo com suas necessidades, dependendo do que precisar subir, no meu caso, só o MongoDB, se precisasse de RabbitMQ, também o adicionaria, por exemplo.
No código, é simples, depois de termos as dependências configuradas corretamente, podemos adicionar e instanciar os containers passando a tag
da imagem docker como parâmetro, para o mongodb, mongo:latest
.
- Adicionar o Container:
private static final MongoDBContainer MONGO_DB_CONTAINER = new MongoDBContainer("mongo:latest");
- Adicionar
@Container
à variável do container - Se estiver usando Spring 3.1+, adicionar a anotação
@ServiceConnection
também. Essa configuração pega os dados do container criado e automaticamente sobrescreve com essas informações o container que seria usado originalmente- Caso contrário, precisamos fazer isso na mão: crie um método que recebe como parâmetro
DynamicPropertyRegistry
- Anote esse método com
@DynamicPropertySource
- Altere as propriedades que estavam configuradas anteriormente usando
registry.add("propriedade", valor)
- Caso contrário, precisamos fazer isso na mão: crie um método que recebe como parâmetro
@Container
private static final MongoDBContainer MONGO_DB_CONTAINER = new MongoDBContainer("mongo:latest");
@Container
private static final RabbitMQContainer RABBIT_MQ_CONTAINER = new RabbitMQContainer("rabbitmq:management");
@DynamicPropertySource
static void mongoDbProperties(DynamicPropertyRegistry registry) {
MONGO_DB_CONTAINER.start();
registry.add("spring.data.mongodb.uri", MONGO_DB_CONTAINER::getReplicaSetUrl);
registry.add("spring.rabbitmq.addresses",() -> "amqp://guest:guest@localhost:"+RABBIT_MQ_CONTAINER.getAmqpPort());
}
A partir daí, siga sua vida com seu Rabbit, Mongo ou qualquer outra instância descartável.
@Testcontainers
@SpringBootTest
public class TicketRepositoryJPATest {
@Autowired
private TicketRepositoryJPA ticketRepository;
@Autowired
private TicketFactory ticketFactory;
@Container
// @ServiceConnection spring 3.1+ - makes DynamicPropertySource unnecessary
private static final MongoDBContainer MONGO_DB_CONTAINER = new MongoDBContainer("mongo:latest");
@DynamicPropertySource
static void mongoDbProperties(DynamicPropertyRegistry registry) {
MONGO_DB_CONTAINER.start();
registry.add("spring.data.mongodb.uri", MONGO_DB_CONTAINER::getReplicaSetUrl);
}
@Test
@Order(0)
void InsertTicket_Success(){
Ticket t = ticketFactory.createTicket("validemail@gmail.com", "I have a problem", "hellp");
ticketRepository.insert(t);
}
@Test
@Order(1)
void FindAll_findsOne(){
var tickets = ticketRepository.findAll();
assertEquals(tickets.size(), 1);
}
}
No caso acima faço um teste simples de repositório onde garanto que alguns métodos estão sendo executados corretamente, mas poderia por exemplo, fazer um teste de integração que garante service + repositório, ou até mesmo um teste de integração completo com TestRestTemplate
.
Importanto classes de declaração de Testcontainer
Um padrão comum ao usar o Testcontainers é declarar instâncias de **Container
**como campos estáticos. Frequentemente, esses campos são definidos diretamente na classe de teste. Eles também podem ser declarados em uma classe pai ou em uma interface que o teste implementa:
public interface MyContainers {
@Container
MongoDBContainer mongoContainer = new MongoDBContainer("mongo:5.0");
@Container
Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5");
}
Para mais discussões, não focando só no setup, mas em configurações diferentes, prós e contras, recomendo esse post: https://maciejwalkowiak.com/blog/testcontainers-spring-boot-setup/
Outra ideia interessante é que podemos configurar TestContainers para aplicações rodando em desenvolvimento, vai servir como um docker compose que não precisamos rodar. É legal, mas não gosto muito da abordagem pois usar docker-compose se tornou parte comum dia a dia de muitos devs e possuí fácil leitura e troubleshooting. Se te animar, para explorar esse ponto: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.testcontainers.at-development-time
Referências:
https://spring.io/blog/2023/06/23/improved-testcontainers-support-in-spring-boot-3-1
https://howtodoinjava.com/spring-boot/testcontainers-with-junit-and-spring-boot/