1. 概述
Spring Data JPA 提供了非常便捷的方式来创建数据库查询,并且可以通过内嵌的 H2 数据库进行快速测试。
但在一些场景下,在真实数据库上测试会更加靠谱,特别是当你使用了一些数据库厂商特有的语法或功能时。
本文将演示 如何使用 Testcontainers 来配合 Spring Data JPA 和 PostgreSQL 数据库做集成测试。
我们之前的文章中已经通过 @Query
注解实现了一些数据库查询逻辑,现在我们来对它们进行测试。
2. 配置
要在测试中使用 PostgreSQL 数据库,首先我们需要添加 Testcontainers 的 PostgreSQL 模块依赖,作用域为 test
:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.6</version>
<scope>test</scope>
</dependency>
然后在 src/test/resources
目录下创建一个 application.properties
文件,告诉 Spring 使用正确的驱动类,并在每次测试运行时自动创建数据库 schema:
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.jpa.hibernate.ddl-auto=create
3. 单个测试类中的使用
要在单个测试类中启动 PostgreSQL 实例,我们需要先定义一个容器,然后使用它的连接参数来配置数据源:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@ActiveProfiles("tc")
@ContextConfiguration(initializers = {UserRepositoryTCLiveTest.Initializer.class})
public class UserRepositoryTCLiveTest extends UserRepositoryCommon {
@ClassRule
public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11.1")
.withDatabaseName("integration-tests-db")
.withUsername("sa")
.withPassword("sa");
@Test
@Transactional
public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers() {
userRepository.save(new User("SAMPLE", LocalDate.now(), USER_EMAIL, ACTIVE_STATUS));
userRepository.save(new User("SAMPLE1", LocalDate.now(), USER_EMAIL2, ACTIVE_STATUS));
userRepository.save(new User("SAMPLE", LocalDate.now(), USER_EMAIL3, ACTIVE_STATUS));
userRepository.save(new User("SAMPLE3", LocalDate.now(), USER_EMAIL4, ACTIVE_STATUS));
userRepository.flush();
int updatedUsersSize = userRepository.updateUserSetStatusForNameNativePostgres(INACTIVE_STATUS, "SAMPLE");
assertThat(updatedUsersSize).isEqualTo(2);
}
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());
}
}
}
在这个例子中,我们使用了 JUnit 的 @ClassRule
来确保在测试方法执行前容器已经启动。同时,我们还创建了一个静态内部类来实现 ApplicationContextInitializer
接口,并通过 @ContextConfiguration
注解将其应用到测试类中。
✅ 这三步操作可以确保在 Spring 上下文启动前就配置好数据库连接信息。
接着我们使用之前文章中定义的两个 UPDATE 查询:
@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);
然后在配置好的环境中进行测试:
@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();
}
在这个测试中,第一个 JPQL 查询顺利通过,但第二个原生 SQL 查询抛出了异常:
Caused by: org.postgresql.util.PSQLException: ERROR: column "u" of relation "users" does not exist
如果我们在 H2 数据库中运行这两个测试,都能通过。但 PostgreSQL 不支持在 SET
子句中使用别名,所以需要修改原生 SQL:
@Modifying
@Query(value = "UPDATE Users u SET status = ? WHERE u.name = ?",
nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);
修改后,两个测试都能通过 ✅。
⚠️ 这个例子很好地说明了使用 Testcontainers 可以提前发现生产环境才会暴露的问题,比如数据库兼容性问题。
另外,使用 JPQL 查询通常更安全,因为 Spring 会根据数据库类型自动转换语法。
3.1. 每个测试类使用一个数据库实例
到目前为止,我们使用的是 JUnit 4 的 @ClassRule
来在测试类执行前启动数据库容器。
这种方式可以保证每个测试类使用独立的数据库实例,隔离性最强,但缺点是启动开销大,测试会变慢。
除了这种方式,我们还可以通过修改 JDBC URL 的方式,让 Testcontainers 自动为每个测试类启动一个数据库实例,而无需写额外的初始化代码。
比如,我们只需要在 application.properties
中添加:
spring.datasource.url=jdbc:tc:postgresql:11.1:///integration-tests-db
tc:
前缀会告诉 Testcontainers 自动启动容器。这样测试类就可以简化为:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserRepositoryTCJdbcLiveTest extends UserRepositoryCommon {
@Test
@Transactional
public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers() {
// same as above
}
}
如果每个测试类都只需要一个数据库实例,这种方式更简洁 ✅。
4. 共享数据库实例
前面的例子都是在单个测试类中使用 Testcontainers。但在实际项目中,我们更希望多个测试类共享同一个数据库容器,以减少启动时间。
为此,我们可以创建一个继承自 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() {
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
}
}
在 stop()
方法中我们留空,让 JVM 自动处理容器关闭。通过单例模式,只有第一个测试类会启动容器,后续测试类复用该实例。
在 start()
方法中,我们将连接信息设置为系统属性。
然后在 application.properties
中引用这些变量:
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
接着在测试类中使用这个工具类:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@ActiveProfiles({"tc", "tc-auto"})
public class UserRepositoryTCAutoLiveTest extends UserRepositoryCommon {
@ClassRule
public static PostgreSQLContainer<BaeldungPostgresqlContainer> postgreSQLContainer = BaeldungPostgresqlContainer.getInstance();
//tests
}
和之前一样,使用 @ClassRule
注解定义容器实例,这样 Spring 上下文创建前就能获取正确的数据库连接信息。
✅ 通过这种方式,我们可以轻松地在多个测试类之间共享同一个数据库实例。
5. 总结
本文介绍了如何使用 Testcontainers 在真实数据库环境中进行集成测试。
我们展示了以下几种方式:
- 单个测试类使用 Testcontainers
- 使用 Spring 的
ApplicationContextInitializer
机制动态配置数据源 - 创建可复用的数据库容器类,实现多个测试类共享实例
此外,我们还展示了 Testcontainers 如何帮助我们提前发现数据库兼容性问题,特别是原生 SQL 查询在不同数据库之间的差异。
📚 完整代码示例可以在这里找到:GitHub 仓库