使用 Spring Boot 和 Testcontainers 进行数据库集成测试
使用 Spring Data JPA,您可以轻松创建数据库查询并使用嵌入式 H2 数据库对其进行测试。
但有时,针对真实数据库进行测试更有用,尤其是当我们使用与特定数据库实现相关联的查询时。
在本教程中,我们将向您展示如何使用 Testcontainers 与 Spring Data JPA 和 PostgreSQL 数据库进行集成测试。
我们将测试使用@QuerySpring Data JPA 中的注释创建的 JPQL 和本机 SQL 查询。
配置
为了在我们的测试中使用PostgreSQL 数据库,我们必须将 test-only Testcontainers依赖项和 PostgreSQL 驱动程序添加到我们的域名文件中:
<dependency> <groupId>域名containers</groupId> <artifactId>postgresql</artifactId> <version>域名.6</version> <scope>test</scope> </dependency> <dependency> <groupId>域名gresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.5</version> </dependency>
我们还将域名erties在测试资源目录中创建一个文件,我们将在其中告诉 Spring 使用所需的驱动程序类,以及在每次运行测试时创建和删除数据库模式:
域名er-class-name=域名er 域名域名-auto=create-drop
单元测试
要在单个测试类中开始使用 PostgreSQL 实例,您需要创建容器定义,然后使用其参数建立连接:
@RunWith(域名s) @SpringBootTest @ContextConfiguration(initializers = {域名s}) 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) { 域名( "域名=" + 域名dbcUrl(), "域名name=" + 域名sername(), "域名word=" + 域名assword() ).applyTo(域名nvironment()); } } }
在上面的示例中,我们@ClassRule在执行测试方法之前使用了 JUnit 来设置数据库容器。我们还创建了一个实现ApplicationContextInitializer. 最后,我们将@ContextConfiguration注解应用于我们的测试类,并将初始化类作为参数。
完成这三个步骤后,我们就可以在发布Spring上下文之前设置连接参数了。
现在我们使用两个UPDATE 查询:
@Modifying @Query("update User u set 域名us = :status where 域名 = :name") int updateUserSetStatusForName(@Param("status") Integer status, @Param("name") String name); @Modifying @Query(value = "UPDATE Users u SET 域名us = ? WHERE 域名 = ?", nativeQuery = true) int updateUserSetStatusForNameNative(Integer status, String name);
并在配置的运行环境中进行测试:
@Test @Transactional public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationJPQL_ThenModifyMatchingUsers(){ insertUsers(); int updatedUsersSize = 域名teUserSetStatusForName(0, "SAMPLE"); assertThat(updatedUsersSize).isEqualTo(2); } @Test @Transactional public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers(){ insertUsers(); int updatedUsersSize = 域名teUserSetStatusForNameNative(0, "SAMPLE"); assertThat(updatedUsersSize).isEqualTo(2); } private void insertUsers() { 域名(new User("SAMPLE", "email@域名", 1)); 域名(new User("SAMPLE1", "email2@域名", 1)); 域名(new User("SAMPLE", "email3@域名", 1)); 域名(new User("SAMPLE3", "email4@域名", 1)); 域名h(); }
在上面的脚本中,第一个测试成功,第二个抛出一个InvalidDataAccessResourceUsageException 消息:
Caused by: 域名.PSQLException: ERROR: column "u" of relation "users" does not exist
如果我们使用嵌入式 H2 数据库运行相同的测试,两者都会成功,但 PostgreSQL 不接受语句中的别名SET 。我们可以通过删除有问题的别名来快速修复请求:
@Modifying @Query(value = "UPDATE Users u SET status = ? WHERE 域名 = ?", nativeQuery = true) int updateUserSetStatusForNameNative(Integer status, String name);
这次两个测试都成功通过了。在此示例中,我们使用 Testcontainer 来检测本机查询的问题,否则只有在迁移到生产数据库后才能发现该问题。还应该注意的是,使用 JPQL 查询通常更安全,因为 Spring 会根据所使用的数据库提供程序正确地转换它们。
共享数据库实例
在上一节中,我们描述了如何在单元测试中使用 Testcontainer 。在实际案例中,由于启动时间比较长,我希望在多次测试中使用同一个数据库容器。
PostgreSQLContainer 让我们通过继承和覆盖start()和方法来创建一个用于创建数据库容器的通用类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() { 域名t(); 域名roperty("DB_URL", 域名dbcUrl()); 域名roperty("DB_USERNAME", 域名sername()); 域名roperty("DB_PASSWORD", 域名assword()); } @Override public void stop() { //do nothing, JVM handles shut down } }
将该stop()方法留空允许 JVM 处理容器本身的终止。
我们还将实现一个简单的单例,其中只有第一个测试启动容器,每个后续测试都使用现有实例。
在该start()方法中,我们使用System#setProperty将连接设置存储在环境变量中。
现在我们可以将它们写入文件域名erties:
域名=${DB_URL} 域名name=${DB_USERNAME} 域名word=${DB_PASSWORD}
现在我们在测试定义中使用我们的实用程序类:
\0
@RunWith(域名s) @SpringBootTest public class UserRepositoryTCAutoIntegrationTest { @ClassRule public static PostgreSQLContainer postgreSQLContainer = 域名nstance(); // tests }
与前面的示例一样,我们已将@ClassRule注释应用于具有容器定义的字段。这样,在创建 Spring 上下文之前用正确的值填充数据源连接参数。
我们现在可以通过简单地定义一个@ClassRule用我们的BaeldungPostgresqlContainer 实用程序类创建的带注释的字段来使用同一个数据库实例实现多个测试。
结论
在本文中,我们展示了使用 Testcontainer 在生产数据库上进行的测试方法。
我们还查看了使用 Spring 机制进行单元测试的示例ApplicationContextInitializer ,以及用于重用数据库实例的类实现。
我们还展示了测试容器如何帮助识别多个数据库供应商之间的兼容性问题,尤其是对于本机查询。