1. Présentation
Avec Spring Data JPA, vous pouvez facilement crĂ©er des requĂȘtes de base de donnĂ©es et les tester Ă l'aide de la base de donnĂ©es H2 intĂ©grĂ©e.
Mais parfois, tester sur une vraie base de donnĂ©es est beaucoup plus utile , surtout si nous utilisons des requĂȘtes liĂ©es Ă une implĂ©mentation de base de donnĂ©es spĂ©cifique.
Dans ce guide, nous montrerons comment utiliser Testcontainers pour les tests d'intégration avec Spring Data JPA et la base de données PostgreSQL.
Dans l'article prĂ©cĂ©dent, nous avons créé plusieurs requĂȘtes de base de donnĂ©es, en utilisant principalement l'annotation @Query , que nous testons actuellement.
2. Configuration
Pour utiliser la base de données PostgreSQL dans nos tests, nous devons ajouter la dépendance Testcontainers des seuls tests et le pilote PostgreSQL à notre fichier 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>
Nous créerons également un fichier application.properties dans le répertoire des ressources de test, dans lequel nous définirons l'utilisation de la classe de pilote requise pour Spring, ainsi que la création et la suppression du schéma de base de données à chaque exécution du test:
spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=create-drop
3. Test unitaire
Pour commencer à utiliser une instance PostgreSQL dans une seule classe de test, vous devez créer une définition de conteneur, puis utiliser ses paramÚtres pour établir la connexion:
@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()); } } }
Dans l'exemple ci-dessus, nous avons utilisé @ClassRule
de JUnit pour configurer le conteneur de base de données avant d'exécuter les méthodes de test . Nous avons également créé une classe interne statique qui implémente ApplicationContextInitializer . Enfin, nous avons appliqué l'annotation @ContextConfiguration
Ă notre classe de test avec la classe d'initialisation comme paramĂštre.
Une fois ces trois étapes terminées, nous pouvons définir les paramÚtres de connexion avant de publier le contexte Spring.
Nous utilisons maintenant deux requĂȘtes UPDATE de l'article prĂ©cĂ©dent:
@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);
Et testez dans un environnement d'exécution optimisé:
@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(); }
Dans le scénario ci-dessus, le premier test réussit et le second lÚve une exception InvalidDataAccessResourceUsageException avec le message:
Caused by: org.postgresql.util.PSQLException: ERROR: column "u" of relation "users" does not exist
Si nous exĂ©cutions les mĂȘmes tests en utilisant la base de donnĂ©es H2 intĂ©grĂ©e, les deux rĂ©ussiraient, mais PostgreSQL n'accepte pas les alias dans l'instruction SET. Nous pouvons rĂ©soudre rapidement la demande en supprimant l'alias problĂ©matique:
@Modifying @Query(value = "UPDATE Users u SET status = ? WHERE u.name = ?", nativeQuery = true) int updateUserSetStatusForNameNative(Integer status, String name);
Cette fois, les deux tests ont rĂ©ussi. Dans cet exemple, nous avons utilisĂ© Testcontainers pour identifier un problĂšme avec une requĂȘte native qui ne serait autrement dĂ©tectĂ© qu'aprĂšs avoir accĂ©dĂ© Ă la base de donnĂ©es de production . Il convient Ă©galement de noter que l'utilisation de requĂȘtes JPQL est gĂ©nĂ©ralement plus sĂ»re, car Spring les traduit correctement en fonction du fournisseur de base de donnĂ©es utilisĂ©.
4. Instance de base de données partagée
Dans la section prĂ©cĂ©dente, nous avons dĂ©crit comment utiliser les conteneurs de test dans un seul test. Dans des cas rĂ©els, je voudrais utiliser le mĂȘme conteneur de base de donnĂ©es dans plusieurs tests en raison du temps de lancement relativement long.
Créons une classe commune pour créer un conteneur de base de données en héritant de PostgreSQLContainer et en remplaçant les méthodes start () et 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() {
Laissant la mĂ©thode stop () vide, nous permettons Ă la JVM de gĂ©rer la complĂ©tion du conteneur par elle-mĂȘme. Nous implĂ©mentons Ă©galement un simple singleton, dans lequel seul le premier test dĂ©marre le conteneur, et chaque test suivant utilise une instance existante. Dans la mĂ©thode start () , nous utilisons System # setProperty pour enregistrer les paramĂštres de connexion dans les variables d'environnement.
Nous pouvons maintenant les écrire dans le fichier application.properties :
spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD}
Maintenant, nous utilisons notre classe d'utilité dans la définition de test:
@RunWith(SpringRunner.class) @SpringBootTest public class UserRepositoryTCAutoIntegrationTest { @ClassRule public static PostgreSQLContainer postgreSQLContainer = BaeldungPostgresqlContainer.getInstance();
Comme dans les exemples précédents, nous avons appliqué l'annotation @ClassRule au champ avec la définition de conteneur. Ainsi, les paramÚtres de connexion DataSource sont remplis avec les valeurs correctes avant de créer le contexte Spring.
Nous pouvons maintenant implĂ©menter plusieurs tests en utilisant la mĂȘme instance de base de donnĂ©es en dĂ©finissant simplement le champ avec l'annotation @ClassRule créée Ă l'aide de notre classe utilitaire BaeldungPostgresqlContainer .
5. Conclusion
Dans cet article, nous avons montré des méthodes de test sur une base de données de production à l'aide de Testcontainers.
Nous avons également examiné des exemples d'utilisation d'un seul test à l'aide du mécanisme ApplicationContextInitializer de Spring, ainsi que l'implémentation d'une classe pour réutiliser une instance de base de données.
Nous avons Ă©galement montrĂ© comment les Testcontainers peuvent aider Ă identifier les problĂšmes de compatibilitĂ© entre plusieurs fournisseurs de bases de donnĂ©es, en particulier pour les requĂȘtes natives.
Comme toujours, le code complet utilisé dans cet article est disponible sur GitHub .