嵌入式Postgres用于Spring Boot测试

42

我正在构建一个使用Spring Boot框架,后端使用Postgres数据库,并使用Flyway进行数据库迁移的应用程序。我一直在遇到问题,无法生成既能在Postgres中也能在内嵌的单元测试数据库中产生所需结果的迁移(即使启用了Postgres兼容模式)。因此,我正在考虑在单元测试中使用嵌入式Postgres。

我发现一个有希望的嵌入式postgres实现,但不确定如何设置它仅在Spring Boot的单元测试框架中运行(用于测试Spring Data存储库)。如何使用上述工具或替代版本的嵌入式Postgres进行设置呢?


2
为什么不直接使用你实际在生产中使用的Postgres数据库,这样你的代码就可以与之配合工作了呢? - JB Nizet
5
是的,还有其他选择,但我们更喜欢像@DataJpaTest这样的单元测试可以在本地计算机上运行而无需安装数据库。 - SingleShot
4
主要原因是 CI/CD 管道。当您运行测试作为 CI/CD 管道的一部分时,通常处于与外部资源隔离的环境中,您不能或者不应该访问外部资源。此外,数据库可能有安全协议,您不想将其注入到 CI 管道容器中。还有很多其他原因,但这是最具说服力的一个。 - aeskreis
5个回答

59

我是embedded-database-spring-test库的作者,这个库被@MartinVolejnik提到。我认为这个库应该符合您的所有需求(PostgreSQL + Spring Boot + Flyway + 集成测试)。很抱歉您遇到了一些麻烦,因此我创建了一个简单演示应用程序,展示了如何使用这个库与Spring Boot框架。下面我总结了一些您需要做的基本步骤。

Maven配置

添加以下Maven依赖:

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

Flyway配置

将以下属性添加到您的应用程序配置中:

# Sets the schemas managed by Flyway -> change the xxx value to the name of your schema
# flyway.schemas=xxx // for spring boot 1.x.x
spring.flyway.schemas=xxx // for spring boot 2.x.x

此外,请确保不要使用org.flywaydb.test.junit.FlywayTestExecutionListener。因为该库有自己的测试执行监听器,可以优化数据库初始化,如果应用了FlywayTestExecutionListener,则此优化将无效。 示例 演示嵌入式数据库使用的测试类示例:
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureEmbeddedDatabase
public class SpringDataJpaAnnotationTest {

    @Autowired
    private PersonRepository personRepository;

    @Test
    public void testEmbeddedDatabase() {
        Optional<Person> personOptional = personRepository.findById(1L);

        assertThat(personOptional).hasValueSatisfying(person -> {
            assertThat(person.getId()).isNotNull();
            assertThat(person.getFirstName()).isEqualTo("Dave");
            assertThat(person.getLastName()).isEqualTo("Syer");
        });
    }
}

1
谢谢。我的所有测试都通过了!但是日志中有很多错误:无法从PostgreSQL JDBC Driver 42.1.1为postgres在jdbc:postgresql://localhost:54436/postgres创建非池化数据源:org.postgresql.util.PSQLException:连接到localhost:54436被拒绝。请检查主机名和端口是否正确,并且postmaster接受TCP/IP连接。 致命错误:数据库系统正在启动 但它仍然可以工作。 - SingleShot
1
很遗憾,otj-pg-embedded 组件的最新稳定版本仍在使用 9.4.1208 版本的 jdbc 驱动程序。第一个使用 42.1.x postgres 驱动程序的版本是 otj-pg-embedded:0.11.1,但它依赖于 Spring Boot 的里程碑版本,并且不是 Maven 中央库的一部分。 - Tomáš Vaněk
1
@JonathanJohx 感谢您的反馈。目前还没有重用以前创建或现有容器的选项,您只能更改镜像。无论如何,在我的情况下,一个新的容器大约需要5秒钟,所以每次都重新开始它应该不是什么大问题。但是,如果您有不同的意见,请随时在 github 项目 上创建功能请求。 - Tomáš Vaněk
1
@JonathanJohx 哦,现在我明白了。不幸的是,没有办法保留已经创建的容器。加快数据库加载速度的唯一方法是将数据直接烘焙到 template1 数据库中的容器中。这样所有测试数据库都将包含准备好的数据,flyway 迁移将更快。 - Tomáš Vaněk
1
@sanjeev 是的,当然可以。这是一个正常的功能齐全的Postgres数据库。 - Tomáš Vaněk
显示剩余15条评论

12

另一个相当简洁的解决该问题的方法是使用 TestContainers 库。唯一需要注意的是它需要 Docker。

集成测试:

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(initializers = {ApplicationTestsIT.Initializer.class})
public class ApplicationTestsIT {

    private static int POSTGRES_PORT = 5432;

    @Autowired
    private FooRepository fooRepository;

    @ClassRule
    public static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres")
            .withDatabaseName("foo")
            .withUsername("it_user")
            .withPassword("it_pass")
            .withInitScript("sql/init_postgres.sql");

    static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues.of(
                    "spring.data.postgres.host=" + postgres.getContainerIpAddress(),
                    "spring.data.postgres.port=" + postgres.getMappedPort(POSTGRES_PORT),
                    "spring.data.postgres.username=" + postgres.getUsername(),
                    "spring.data.postgres.password=" + postgres.getPassword()
            ).applyTo(configurableApplicationContext.getEnvironment());
        }
    }

    @Test
    public void fooRepositoryTestIT() {
        ...
    }

依赖配置:
pom.xml:

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

build.gradle

testCompile "org.testcontainers:postgresql:x.x.x"

链接:
TestContainers - 数据库
TestContainers - Postgres 模块


4
下面的配置在Spring Boot 2.0中运行良好。
embedded-database-spring-test相比的优点是,这个解决方案不会将Flyway推入类路径中,可能会干扰Spring Boot的自动配置。
@Configuration
@Slf4j
public class EmbeddedPostgresConfiguration {

    @Bean(destroyMethod = "stop")
    public PostgresProcess postgresProcess() throws IOException {
        log.info("Starting embedded Postgres");

        String tempDir = System.getProperty("java.io.tmpdir");
        String dataDir = tempDir + "/database_for_tests";
        String binariesDir = System.getProperty("java.io.tmpdir") + "/postgres_binaries";

        PostgresConfig postgresConfig = new PostgresConfig(
                Version.V10_3,
                new AbstractPostgresConfig.Net("localhost", Network.getFreeServerPort()),
                new AbstractPostgresConfig.Storage("database_for_tests", dataDir),
                new AbstractPostgresConfig.Timeout(60_000),
                new AbstractPostgresConfig.Credentials("bob", "ninja")
        );

        PostgresStarter<PostgresExecutable, PostgresProcess> runtime =
                PostgresStarter.getInstance(EmbeddedPostgres.cachedRuntimeConfig(Paths.get(binariesDir)));
        PostgresExecutable exec = runtime.prepare(postgresConfig);
        PostgresProcess process = exec.start();

        return process;
    }

    @Bean(destroyMethod = "close")
    @DependsOn("postgresProcess")
    DataSource dataSource(PostgresProcess postgresProcess) {
        PostgresConfig postgresConfig = postgresProcess.getConfig();

        val config = new HikariConfig();
        config.setUsername(postgresConfig.credentials().username());
        config.setPassword(postgresConfig.credentials().password());
        config.setJdbcUrl("jdbc:postgresql://localhost:" + postgresConfig.net().port() + "/" + postgresConfig.storage().dbName());

        return new HikariDataSource(config);
    }
}

Maven:

        <dependency>
            <groupId>ru.yandex.qatools.embed</groupId>
            <artifactId>postgresql-embedded</artifactId>
            <version>2.9</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>

该类基于我在这里找到的代码:https://github.com/nkoder/postgresql-embedded-example
我进行了修改,以使用HikariDatasource(Spring Boot的默认数据源)来实现正确的连接池。 binariesDirdataDir用于避免在重复测试中进行昂贵的提取+initdb操作。

1
底层的 https://github.com/yandex-qatools/postgresql-embedded 已经不再得到积极维护。他们建议切换到 https://www.testcontainers.org/modules/databases/postgres/,但这只是一个选项,如果您的开发环境中有 Docker 或者远程端口可以访问 Docker 引擎。 - dschulten
你是正确的。自从我写下这个答案以来,我已经使用了Testcontainers几次。对于大多数项目来说,Testcontainers似乎是更好的工具。唯一的缺点可能是对Docker的依赖性。 - Mateusz Stefek

3
请看这个链接:https://github.com/zonkyio/embedded-database-spring-test。需要说明的是,它是用于集成测试的。这意味着Spring上下文在每个测试期间都会被初始化。
根据工具文档,你只需要在类上方添加@AutoConfigureEmbeddedDatabase注释即可:
@RunWith(SpringRunner.class)
@AutoConfigureEmbeddedDatabase
@ContextConfiguration("/path/to/app-config.xml")
public class FlywayMigrationIntegrationTest {

    @Test
    @FlywayTest(locationsForMigrate = "test/db/migration")
    public void testMethod() {
        // method body...
    }
}

并添加Maven依赖项:

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

为了和 @DataJpaTest 一起使用,您需要使用注释 @AutoConfigureTestDatabase(replace = NONE) 来禁用默认测试数据库:
@RunWith(SpringRunner.class)
@AutoConfigureTestDatabase(replace = NONE)
@AutoConfigureEmbeddedDatabase
@DataJpaTest
public class SpringDataJpaTest {
// class body...
}

为了让使用更加舒适,您可以创建一个组合注释,例如:
@Documented
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@AutoConfigureTestDatabase(replace = NONE)
@AutoConfigureEmbeddedDatabase
@DataJpaTest
public @interface PostgresDataJpaTest {
}

然后在你的测试类上方使用它:

@RunWith(SpringRunner.class)
@PostgresDataJpaTest // custom composite annotation
public class SpringDataJpaTest {
// class body...
}

谢谢,我会去看一下。不过,如果它不能在@DataJpaTest下运行,我就得找别的方法了。 - SingleShot
@SingleShot 我已经根据你的评论编辑了我的答案。在我上一个项目中,我们广泛使用了这个库,它基本上涵盖了我们所有的测试需求。我强烈推荐它。 - Martin Volejnik
谢谢!我正在尝试让它工作,但有些困难。另外一个问题是我们使用Flyway进行迁移,但以上设置无法运行。如果我能解决问题,我会回报的。 - SingleShot
如果您有任何建议,我将不胜感激。谢谢! - SingleShot
@SingleShot 你是否正在使用 @FlywayTest 注解?你可以将其放置在测试方法或测试类上,并且可以在注解中指定迁移路径。 - Martin Volejnik
@SingleShot 我们没有在 @DataJpaTest 中使用它,因为我们使用了完整的上下文,但我找到了这个链接:https://github.com/spring-projects/spring-boot/issues/5716 所以你可能还需要添加 @ImportAutoConfiguration(FlywayAutoConfiguration.class) - Martin Volejnik

0

谢谢,但这完全不是我在这里要问的。 - SingleShot
也许我没有理解你的问题。嗯... 我还是不明白。我以为你是在寻找Spring Boot的嵌入式PostgreSQL配置。DockDS就是这样一个东西。它由Docker支持,但这是无缝的,并且可以在Travis和GitlabCI等CI工具中很好地工作。数据库实例的生命周期与Spring应用程序上下文相连接。 - Tomasz Wielga
2
嗨Tomasz。我所说的“嵌入式”是指它在Java单元测试中运行。Spring Boot为单元测试提供了3个嵌入式数据库,但我正在寻找一种使用Postgres的替代方案。目标是任何人都可以构建和测试我们的应用程序,而无需安装任何Java和Maven。谢谢。 - SingleShot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接