Testes de integração de banco de dados com Spring Boot e Testcontainers
1. Visão geral
O Spring Data JPA fornece uma maneira fácil de criar consultas ao banco de dados e testá-las com um banco de dados H2 incorporado.
Mas, em alguns casos,testing on a real database is much more profitable,, especialmente se usarmos consultas dependentes do provedor.
Neste tutorial, demonstraremoshow to use Testcontainers for integration testing with Spring Data JPA and the PostgreSQL database.
Em nosso tutorial anterior, criamos alguns bancos de dadosqueries using mainly the @Query annotation, que agora iremos testar.
2. Configuração
Para usar o banco de dados PostgreSQL em nossos testes,we have to add the Testcontainers dependency with test scopeePostgreSQL driver para nossopom.xml:
org.testcontainers
postgresql
1.10.6
test
org.postgresql
postgresql
42.2.5
Vamos também criar um arquivoapplication.properties no diretório de recursos de teste no qual instruímos o Spring a usar a classe de driver adequada e a criar e eliminar o esquema em cada execução de teste:
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=create-drop
3. Uso de teste único
Para começar a usar a instância do PostgreSQL em uma única classe de teste, primeiro precisamos criar uma definição de contêiner e depois usar seus parâmetros para estabelecer uma conexão:
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(initializers = {UserRepositoryTCIntegrationTest.Initializer.class})
public class UserRepositoryTCIntegrationTest extends UserRepositoryCommonIntegrationTests {
@ClassRule
public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11.1")
.withDatabaseName("integration-tests-db")
.withUsername("sa")
.withPassword("sa");
static class Initializer
implements ApplicationContextInitializer {
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(),
"spring.datasource.username=" + postgreSQLContainer.getUsername(),
"spring.datasource.password=" + postgreSQLContainer.getPassword()
).applyTo(configurableApplicationContext.getEnvironment());
}
}
}
No exemplo acima, usamos@ClassRule do JUnit para configurar um contêiner de banco de dadosbefore executing test methods. Também criamos uma classe interna estática que implementaApplicationContextInitializer.. Como última etapa, aplicamos a anotação@ContextConfiguration à nossa classe de teste com a classe inicializadora como parâmetro.
Ao realizar essas três ações, podemos definir as propriedades da conexão antes que o contexto Spring seja publicado.
Vamos agora usar duas consultas UPDATE do artigo anterior:
@Modifying
@Query("update User u set u.status = :status where u.name = :name")
int updateUserSetStatusForName(@Param("status") Integer status,
@Param("name") String name);
@Modifying
@Query(value = "UPDATE Users u SET u.status = ? WHERE u.name = ?",
nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);
E teste-os com o ambiente configurado:
@Test
@Transactional
public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationJPQL_ThenModifyMatchingUsers(){
insertUsers();
int updatedUsersSize = userRepository.updateUserSetStatusForName(0, "SAMPLE");
assertThat(updatedUsersSize).isEqualTo(2);
}
@Test
@Transactional
public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers(){
insertUsers();
int updatedUsersSize = userRepository.updateUserSetStatusForNameNative(0, "SAMPLE");
assertThat(updatedUsersSize).isEqualTo(2);
}
private void insertUsers() {
userRepository.save(new User("SAMPLE", "[email protected]", 1));
userRepository.save(new User("SAMPLE1", "[email protected]", 1));
userRepository.save(new User("SAMPLE", "[email protected]", 1));
userRepository.save(new User("SAMPLE3", "[email protected]", 1));
userRepository.flush();
}
No cenário acima, o primeiro teste termina com sucesso, mas o segundo lançaInvalidDataAccessResourceUsageException com a mensagem:
Caused by: org.postgresql.util.PSQLException: ERROR: column "u" of relation "users" does not exist
Se tivéssemos executado os mesmos testes usando o banco de dados integrado H2, ambos os testes seriam concluídos com sucesso, mas o PostgreSQL não aceita apelidos na cláusula SET. Podemos corrigir rapidamente a consulta removendo o alias problemático:
@Modifying
@Query(value = "UPDATE Users u SET status = ? WHERE u.name = ?",
nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);
Desta vez, ambos os testes foram concluídos com sucesso. Neste exemplo,we used Testcontainers to identify a problem with the native query which otherwise would be revealed after switching to a real database on production. Também devemos notar que usar consultasJPQL é mais seguro em geral porque o Spring as traduz corretamente dependendo do provedor de banco de dados usado.
4. Instância de banco de dados compartilhado
No parágrafo anterior, descrevemos como usar os contêineres de teste em um único teste. Em um cenário de caso real, gostaríamos de reutilizar o mesmo contêiner de banco de dados em vários testes devido ao tempo de inicialização relativamente longo.
Vamos agora criar uma classe comum para a criação de contêiner de banco de dados estendendoPostgreSQLContainere substituindo os métodosstart()estop():
public class examplePostgresqlContainer extends PostgreSQLContainer {
private static final String IMAGE_VERSION = "postgres:11.1";
private static examplePostgresqlContainer container;
private examplePostgresqlContainer() {
super(IMAGE_VERSION);
}
public static examplePostgresqlContainer getInstance() {
if (container == null) {
container = new examplePostgresqlContainer();
}
return container;
}
@Override
public void start() {
super.start();
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
}
}
Ao deixar o métodostop() vazio, permitimos que a JVM trate do encerramento do contêiner. Também implementamos um padrão simples de singleton, no qual apenas o primeiro teste aciona a inicialização do contêiner e cada teste subsequente usa a instância existente. No métodostart(), usamosSystem#setProperty para definir os parâmetros de conexão como variáveis de ambiente.
Agora podemos colocá-los em nossoapplication.properties file:
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
Vamos agora usar nossa classe de utilitário na definição de teste:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRepositoryTCAutoIntegrationTest {
@ClassRule
public static PostgreSQLContainer postgreSQLContainer = examplePostgresqlContainer.getInstance();
// tests
}
Como nos exemplos anteriores, aplicamosthe @ClassRule annotation a um campo que contém a definição do contêiner. Dessa forma, as propriedades de conexãoDataSource são preenchidas com os valores corretos antes da criação do contexto Spring.
We can now implement multiple tests using the same database instance simplesmente definindo um campo anotado@ClassRule instanciado com nossa classe de utilidadeexamplePostgresqlContainer.
5. Conclusão
Neste artigo, ilustramos maneiras de executar testes em uma instância de banco de dados real usando Testcontainers.
Vimos exemplos de uso de teste único, usando o mecanismoApplicationContextInitializer do Spring, bem como implementando uma classe para instanciação de banco de dados reutilizável.
Também mostramos como os Testcontainers podem ajudar na identificação de problemas de compatibilidade em vários provedores de banco de dados, especialmente para consultas nativas.
Como sempre, o código completo usado neste artigo está disponívelover on GitHub.