Testes de integração de banco de dados com Spring Boot e Testcontainers

1. Visão geral


Com o Spring Data JPA, você pode facilmente criar consultas ao banco de dados e testá-las usando o banco de dados H2 interno.


Às vezes, porém, testar em um banco de dados real é muito mais útil , especialmente se usarmos consultas vinculadas a uma implementação específica do banco de dados.


Neste tutorial, mostraremos como usar Testcontainers para testes de integração com o banco de dados Spring Data JPA e PostgreSQL.


No artigo anterior, criamos várias consultas ao banco de dados, usando principalmente a anotação @Query , que estamos testando agora.


2. Configuração


Para usar o banco de dados PostgreSQL em nossos testes, devemos adicionar a dependência Testcontainers apenas de testes e o driver PostgreSQL ao nosso arquivo pom.xml :


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

Também criaremos o arquivo application.properties no diretório de recursos de teste, no qual definiremos o uso da classe de driver necessária para o Spring, além de criar e excluir o esquema do banco de dados toda vez que o teste for executado:


 spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=create-drop 

3. Teste de unidade


Para começar a usar uma instância do PostgreSQL em uma única classe de teste, você precisa criar uma definição de contêiner e, em seguida, usar seus parâmetros para estabelecer a 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<ConfigurableApplicationContext> { 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 da JUnit para configurar o contêiner do banco de dados antes de executar os métodos de teste . Também criamos uma classe interna estática que implementa ApplicationContextInitializer . Por fim, aplicamos a anotação @ContextConfiguration à nossa classe de teste com uma classe de inicialização como parâmetro.


Após concluir essas três etapas, podemos definir os parâmetros de conexão antes de publicar o contexto do Spring.


Agora, usamos 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 em um ambiente de tempo de execução ajustado:


 @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@example.com", 1)); userRepository.save(new User("SAMPLE1", "email2@example.com", 1)); userRepository.save(new User("SAMPLE", "email3@example.com", 1)); userRepository.save(new User("SAMPLE3", "email4@example.com", 1)); userRepository.flush(); } 

No cenário acima, o primeiro teste é bem-sucedido e o segundo lança um InvalidDataAccessResourceUsageException com a mensagem:


 Caused by: org.postgresql.util.PSQLException: ERROR: column "u" of relation "users" does not exist 

Se executássemos os mesmos testes usando o banco de dados H2 embutido, ambos teriam êxito, mas o PostgreSQL não aceita apelidos na instrução SET. Podemos corrigir rapidamente a solicitação 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 aprovados com sucesso. Neste exemplo, usamos Testcontainers para identificar um problema com uma consulta nativa que, de outra forma, seria detectada somente após acessar o banco de dados de produção . Também deve-se notar que o uso de consultas JPQL geralmente é mais seguro, pois o Spring as traduz corretamente, dependendo do provedor de banco de dados usado.


4. Instância de banco de dados compartilhada


Na seção anterior, descrevemos como usar os contêineres de teste em um único teste. Em casos reais, eu gostaria de usar o mesmo contêiner de banco de dados em vários testes devido ao tempo de inicialização relativamente longo.


Vamos criar uma classe geral para criar um contêiner de banco de dados herdando PostgreSQLContainer e substituindo os métodos start () e stop () :


 public class BaeldungPostgresqlContainer extends PostgreSQLContainer<BaeldungPostgresqlContainer> { private static final String IMAGE_VERSION = "postgres:11.1"; private static BaeldungPostgresqlContainer container; private BaeldungPostgresqlContainer() { super(IMAGE_VERSION); } public static BaeldungPostgresqlContainer getInstance() { if (container == null) { container = new BaeldungPostgresqlContainer(); } 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 } } 

Deixando o método stop () vazio, habilitamos a JVM a lidar com a conclusão do contêiner por conta própria. Também implementamos um singleton simples, no qual apenas o primeiro teste inicia o contêiner e cada teste subsequente usa uma instância existente. No método start () , usamos System # setProperty para salvar os parâmetros de conexão em variáveis ​​de ambiente.


Agora podemos gravá-los no arquivo application.properties :


 spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD} 

Agora usamos nossa classe de utilitário na definição de teste:


 @RunWith(SpringRunner.class) @SpringBootTest public class UserRepositoryTCAutoIntegrationTest { @ClassRule public static PostgreSQLContainer postgreSQLContainer = BaeldungPostgresqlContainer.getInstance(); // tests } 

Como nos exemplos anteriores, aplicamos a anotação @ClassRule ao campo com a definição do contêiner. Portanto, os parâmetros de conexão do DataSource são preenchidos com os valores corretos antes de criar o contexto do Spring.


Agora podemos implementar vários testes usando a mesma instância de banco de dados, basta definir o campo com a anotação @ClassRule criada usando nossa classe de utilitário BaeldungPostgresqlContainer .


5. Conclusão


Neste artigo, mostramos métodos de teste em um banco de dados de produção usando Testcontainers.


Também examinamos exemplos de uso de um único teste usando o mecanismo ApplicationContextInitializer da Spring, além de implementar uma classe para reutilizar uma instância de banco de dados.


Também mostramos como os Testcontainers podem ajudar a identificar problemas de compatibilidade entre vários provedores de banco de dados, especialmente para consultas nativas.


Como sempre, o código completo usado neste artigo está disponível no GitHub .

Source: https://habr.com/ru/post/pt454970/


All Articles