1. 概述

编写数据库集成测试时,测试数据库的选择至关重要。使用真实数据库是最佳实践之一,能确保集成测试最大程度模拟生产环境行为。

本文将演示如何在Spring Boot测试中使用嵌入式PostgreSQL,并对比几种替代方案。

2. 依赖和配置

首先添加Spring Data JPA依赖,用于创建Repository:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Spring Boot应用编写集成测试,需要包含Spring Test依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

最后添加嵌入式PostgreSQL依赖

<dependency>
    <groupId>com.opentable.components</groupId>
    <artifactId>otj-pg-embedded</artifactId>
    <version>1.0.3</version>
    <scope>test</scope>
</dependency>

配置测试环境基本参数:

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=create-drop

✅ 指定了PostgreSQL方言
✅ 启用测试执行前自动重建数据库结构

3. 使用

先创建测试用的Person实体类:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String name;

    // getters and setters
}

创建对应的Spring Data Repository:

public interface PersonRepository extends JpaRepository<Person, Long> {
}

接下来创建测试配置类:

@Configuration
@EnableJpaRepositories(basePackageClasses = PersonRepository.class)
@EntityScan(basePackageClasses = Person.class)
public class EmbeddedPostgresConfiguration {
    private static EmbeddedPostgres embeddedPostgres;

    @Bean
    public DataSource dataSource() throws IOException {
        embeddedPostgres = EmbeddedPostgres.builder()
          .setImage(DockerImageName.parse("postgres:14.1"))
          .start();

        return embeddedPostgres.getPostgresDatabase();
    }

    public static class EmbeddedPostgresExtension implements AfterAllCallback {
        @Override
        public void afterAll(ExtensionContext context) throws Exception {
            if (embeddedPostgres == null) {
                return;
            }
            embeddedPostgres.close();
        }
    }
}

⚠️ 关键点:

  • 指定了Repository和实体类路径
  • 使用EmbeddedPostgres构建器创建数据源,可指定PostgreSQL版本
  • 添加EmbeddedPostgresExtension确保测试类执行后关闭连接

最后创建测试类:

@DataJpaTest
@ExtendWith(EmbeddedPostgresExtension.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ContextConfiguration(classes = {EmbeddedPostgresConfiguration.class})
public class EmbeddedPostgresIntegrationTest {
    @Autowired
    private PersonRepository repository;

    @Test
    void givenEmbeddedPostgres_whenSavePerson_thenSavedEntityShouldBeReturnedWithExpectedFields(){
        Person person = new Person();
        person.setName("New user");

        Person savedPerson = repository.save(person);
        assertNotNull(savedPerson.getId());
        assertEquals(person.getName(), savedPerson.getName());
    }
}

✅ 使用@DataJpaTest注解初始化测试上下文
✅ 通过EmbeddedPostgresExtension扩展测试类
✅ 成功创建并保存Person实体到数据库

4. Flyway集成

Flyway是流行的数据库迁移工具,集成测试中需要包含它。本节演示如何结合嵌入式PostgreSQL使用。

添加依赖

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

在Flyway迁移脚本中定义数据库结构:

CREATE SEQUENCE IF NOT EXISTS person_seq INCREMENT 50;
;

CREATE TABLE IF NOT EXISTS person(
    id bigint NOT NULL,
    name character varying(255)
)
;

创建测试配置:

@Configuration
@EnableJpaRepositories(basePackageClasses = PersonRepository.class)
@EntityScan(basePackageClasses = Person.class)
public class EmbeddedPostgresWithFlywayConfiguration {
    @Bean
    public DataSource dataSource() throws SQLException {
        return PreparedDbProvider
          .forPreparer(FlywayPreparer.forClasspathLocation("db/migrations"))
          .createDataSource();
    }
}

✅ 使用PreparedDbProviderFlywayPreparer指定迁移脚本位置

测试类实现:

@DataJpaTest(properties = { "spring.jpa.hibernate.ddl-auto=none" })
@ExtendWith(EmbeddedPostgresExtension.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ContextConfiguration(classes = {EmbeddedPostgresWithFlywayConfiguration.class})
public class EmbeddedPostgresWithFlywayIntegrationTest {
    @Autowired
    private PersonRepository repository;

    @Test
    void givenEmbeddedPostgres_whenSavePerson_thenSavedEntityShouldBeReturnedWithExpectedFields(){
        Person person = new Person();
        person.setName("New user");

        Person savedPerson = repository.save(person);
        assertNotNull(savedPerson.getId());
        assertEquals(person.getName(), savedPerson.getName());

        List<Person> allPersons = repository.findAll();
        Assertions.assertThat(allPersons).contains(person);
    }
}

⚠️ 禁用spring.jpa.hibernate.ddl-auto让Flyway接管数据库结构变更
✅ 成功保存并检索Person实体

5. 替代方案

5.1. Testcontainers

最新版嵌入式PostgreSQL项目底层使用TestContainers直接使用TestContainers是可行的替代方案

添加依赖

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

创建初始化类配置PostgreSQLContainer

public class TestContainersInitializer implements
  ApplicationContextInitializer<ConfigurableApplicationContext>, AfterAllCallback {

    private static final PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer(
      "postgres:14.1")
      .withDatabaseName("postgres")
      .withUsername("postgres")
      .withPassword("postgres");


    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        postgreSQLContainer.start();

        TestPropertyValues.of(
          "spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(),
          "spring.datasource.username=" + postgreSQLContainer.getUsername(),
          "spring.datasource.password=" + postgreSQLContainer.getPassword()
        ).applyTo(applicationContext.getEnvironment());
    }

    @Override
    public void afterAll(ExtensionContext context) throws Exception {
        if (postgreSQLContainer == null) {
            return;
        }
        postgreSQLContainer.close();
    }
}

✅ 实现ApplicationContextInitializer设置测试上下文属性
✅ 实现AfterAllCallback测试后关闭容器连接

测试类实现:

@DataJpaTest
@ExtendWith(TestContainersInitializer.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ContextConfiguration(initializers = TestContainersInitializer.class)
public class TestContainersPostgresIntegrationTest {
    @Autowired
    private PersonRepository repository;

    @Test
    void givenTestcontainersPostgres_whenSavePerson_thenSavedEntityShouldBeReturnedWithExpectedFields() {
        Person person = new Person();
        person.setName("New user");

        Person savedPerson = repository.save(person);
        assertNotNull(savedPerson.getId());
        assertEquals(person.getName(), savedPerson.getName());
    }
}

✅ 使用@ContextConfiguration指定初始化器
✅ 成功在测试容器中保存实体

5.2. Zonky嵌入式数据库

Zonky Embedded Database作为嵌入式PostgreSQL的分支,继续支持无Docker的测试数据库方案

添加依赖

<dependency>
    <groupId>io.zonky.test</groupId>
    <artifactId>embedded-postgres</artifactId>
    <version>2.0.7</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.zonky.test</groupId>
    <artifactId>embedded-database-spring-test</artifactId>
    <version>2.5.1</version>
    <scope>test</scope>
</dependency>

测试类实现:

@DataJpaTest
@AutoConfigureEmbeddedDatabase(provider = ZONKY)
public class ZonkyEmbeddedPostgresIntegrationTest {
    @Autowired
    private PersonRepository repository;

    @Test
    void givenZonkyEmbeddedPostgres_whenSavePerson_thenSavedEntityShouldBeReturnedWithExpectedFields(){
        Person person = new Person();
        person.setName("New user");

        Person savedPerson = repository.save(person);
        assertNotNull(savedPerson.getId());
        assertEquals(person.getName(), savedPerson.getName());
    }
}

✅ 使用@AutoConfigureEmbeddedDatabase注解指定ZONKY提供者
✅ 无需Docker即可使用嵌入式PostgreSQL
✅ 同时支持Embedded和Docker等其他提供者

6. 结论

本文探讨了在测试中使用嵌入式PostgreSQL的方法及替代方案。PostgreSQL测试集成方案多样,无论是否使用Docker容器都能实现。最佳选择取决于具体场景:

  • ✅ 简单场景:直接使用嵌入式PostgreSQL
  • ✅ 需要容器化:Testcontainers
  • ✅ 无Docker环境:Zonky方案

完整源代码可在GitHub获取。


原始标题:Embedded PostgreSQL for Spring Boot Tests | Baeldung