42

I'm building a Spring Boot app, backed by Postgres, using Flyway for database migrations. I've been bumping up against issues where I cannot produce a migration that generates the desired outcome in both Postgres, and the embedded unit test database (even with Postgres compatibility mode enabled). So I am looking at using embedded Postgres for unit tests.

I came across an embedded postgres implementation that looks promising, but don't really see how to set it up to run within Spring Boot's unit test framework only (for testing Spring Data repositories). How would one set this up using the mentioned tool or an alternative embedded version of Postgres?

SingleShot
  • 18,821
  • 13
  • 71
  • 101
  • 2
    Why don't you just use your actual Postgres database, the one you use in production, and the one that you thus want your code to work with? – JB Nizet Feb 23 '18 at 21:57
  • 5
    Ya, there are other options, but we prefer unit tests like `@DataJpaTest` to be runnable without installing a database on the local computer. – SingleShot Feb 26 '18 at 00:40
  • 4
    @JBNizet The main reason would be CI/CD pipelines. When you are running tests are part of a CI/CD pipeline, you typically are within an isolated environment and you cannot or should not access external resources. In addition, databases may have security protocols which you don't want to have to inject into your CI pipeline containers. There are many more reasons but this is the most compelling. – aeskreis Nov 25 '20 at 05:14

5 Answers5

58

I'm the author of the embedded-database-spring-test library that was mentioned by @MartinVolejnik. I think the library should meet all your needs (PostgreSQL + Spring Boot + Flyway + integration testing). I'm really sorry that you're having some troubles, so I've created a simple demo app that demonstrates the use of the library together with Spring Boot framework. Below I summarized some basic steps that you need to do.

Maven configuration

Add the following maven dependency:

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

Flyway configuration

Add the following property to your application configuration:

# 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

Further, make sure that you do not use org.flywaydb.test.junit.FlywayTestExecutionListener. Because the library has its own test execution listener that can optimize database initialization and this optimization has no effect if the FlywayTestExecutionListener is applied.

Example

An example of a test class demonstrating the use of the embedded database:

@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");
        });
    }
}
Tomáš Vaněk
  • 693
  • 5
  • 8
  • 1
    Thanks. My tests all pass! I do have a lot of errors in the logs though: ```Failed to create a Non-Pooling DataSource from PostgreSQL JDBC Driver 42.1.1 for postgres at jdbc:postgresql://localhost:54436/postgres: org.postgresql.util.PSQLException: Connection to localhost:54436 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections. FATAL: the database system is starting up ``` Yet it still works. – SingleShot Feb 27 '18 at 17:18
  • @SingleShot It seems this is caused by the version of the postgres jdbc driver. You are using version `42.1.1` but the `otj-pg-embedded:0.9.0` component we depend on is compiled with version `9.4.1208`. There is probably some incompatibility between this two versions of jdbc drivers, which produces the described errors. – Tomáš Vaněk Feb 27 '18 at 20:25
  • 1
    Unfortunately, the most recent stable release of `otj-pg-embedded` component is still using the jdbc driver in version `9.4.1208`. The first version that uses `42.1.x` postgres driver is `otj-pg-embedded:0.11.1` but it depends on a milestone version of Spring Boot and it is not part of maven central repository. – Tomáš Vaněk Feb 27 '18 at 20:25
  • So the only solution is to downgrade the postgres driver or wait for a new version of `otj-pg-embedded` component that will be compatible with newer versions of postgres drivers. – Tomáš Vaněk Feb 27 '18 at 20:25
  • I'm ok with that. I actually had to use `9.4.1212` to remove the errors. But this is great. Thanks for the help! – SingleShot Feb 27 '18 at 21:44
  • Does the library support Liquibase? – naXa stands with Ukraine May 06 '19 at 07:58
  • Yes, it does. In case there is no Flyway bean in the spring context, an empty database is created and this database can be initialized by any tool, including Liquibase. The only missing feature is the ability to revert the database to its default state, but it is on the roadmap. – Tomáš Vaněk May 07 '19 at 15:54
  • @TomášVaněk your library works with Docker perfectly(zonky/test/database/postgres/docker/image properties). But I don't know when debugging how I can start to test with my database image already created. Could you please give me information about it? I hope to have a flag zonky.test.database.use.image.created = true or something like that, because everytime I am debugging I wait much time in order to load docker/image again. I will be thankful if you can provide me more information. Thanks in advance. – Jonathan JOhx Jul 07 '19 at 18:57
  • 1
    @JonathanJohx thanks for the feedback. At this moment, there is no option to reuse a previously created or existing container, you can only change the image. Anyway in my case a new container starts about 5 seconds, so I think it is no big issue to start it every time fresh. However, if you have a different opinion, feel free to create a feature request on [the github project](https://github.com/zonkyio/embedded-database-spring-test). – Tomáš Vaněk Jul 12 '19 at 08:27
  • @TomášVaněk thanks for your answer, perhaps I could not make you understand. I mean I have a lot of tables in my SQL script , then when I am debugging or it is running, it takes a lot of time to create/insert tables in Image/container docker I guess, so is there some way to keep already created tables/database and image/container created? It takes 2 minutes, if not I can formula a good feature in your repo and perhaps work on that, thanks. – Jonathan JOhx Jul 12 '19 at 21:20
  • 1
    @JonathanJohx Oh, now I get it. Unfortunately, there is no such way to keep already created containers. The only way to speed up loading the database is to bake the data directly into the container into [template1](https://www.postgresql.org/docs/current/manage-ag-templatedbs.html) database. So then all test databases will contain the prepared data and flyway migration will go faster. – Tomáš Vaněk Jul 17 '19 at 08:11
  • Hi Tomas, thank you very much for this wonderful library. I've got it up and running quickly, however I'm stumbling upon an issue with Spring's AuditingEntityListener. It will alter entity tables with two columns created_at and updated_at. I need this for at least some entities, so that I can maintain a version history. The trouble is that in my Flyway migration scripts, these columns aren't added explicitly, and so when I'm running my integration test, it throws an exception because one of those columns is missing. Do you by any chance have a suggestion on this? Thanks in advance. :) – Serg Derbst Jun 03 '20 at 12:41
  • @SergDerbst Could you please [open a new issue](https://github.com/zonkyio/embedded-database-spring-test/issues/new) and provide more details about the problem? If the columns are not defined in the flyway migration, how are these columns created in the production environment? – Tomáš Vaněk Jun 05 '20 at 15:59
  • @TomášVaněk Thanks, maybe I will. But right now I believe this is not an issue with your library, but rather has to do with Spring's EnableJpaAuditing and SpringBootTest annotations. Apparently the necessary events on updating or creating an entity aren't thrown or listened to. I am about to research this first, so maybe later. :) – Serg Derbst Jun 06 '20 at 11:01
  • @TomášVaněk can u help me with this -we use UUID in postgres and when flayway starting migrate i got issue ERROR: function uuid_generate_v4() does not exist .In prod DB we do it with script like this 'create extension "uuid-ossp"; is there any way to do it with your library? – Алексеев станислав Jun 22 '21 at 11:11
  • @Алексеевстанислав Please make sure you are using the latest version of the library. In some older versions there was a bug related with uuid extension but it should already be fixed. I tested it now and everything worked fine. If the problem persists feel free to open an issue here: https://github.com/zonkyio/embedded-postgres-binaries – Tomáš Vaněk Jun 22 '21 at 21:46
  • @TomášVaněk can I use this library to test full text search e.g. by using tsvector or tsquery etc? Ref: https://www.postgresql.org/docs/15/textsearch-tables.html#TEXTSEARCH-TABLES-SEARCH – sanjeev May 28 '23 at 14:53
  • 1
    @sanjeev Yes, sure, you can do that. It's a normal full-featured Postgres database. – Tomáš Vaněk May 30 '23 at 09:07
11

Another quite clean solution to that problem is to use the TestContainers library. The only caveat is that it requires Docker.

Integration Test:

@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() {
        ...
    }

Dependency configuration:
pom.xml:

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

build.gradle:

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

Links:
TestContainers - Databases
TestContainers - Postgres Module

magiccrafter
  • 5,175
  • 1
  • 56
  • 50
4

The configuration below works well with Spring Boot 2.0.

The advantage over embedded-database-spring-test is that this solution doesn't push Flyway into the classpath, possibly messing up Spring Boot's autoconfiguration.

@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>

The class is based on the code I found here: https://github.com/nkoder/postgresql-embedded-example

I modified it to use HikariDatasource (Spring Boot's default) for proper connection pooling. The binariesDir and dataDir are used to avoid costly extraction+initdb in repeated tests.

Mateusz Stefek
  • 3,478
  • 2
  • 23
  • 28
  • 1
    The underlying https://github.com/yandex-qatools/postgresql-embedded is not actively maintained anymore. They suggest to switch to https://www.testcontainers.org/modules/databases/postgres/, but that is only an option if you have docker in your development environment or if a docker engine is reachable on a remote port. – dschulten May 15 '19 at 07:31
  • You are right. I’ve used Testcontainers a couple of times since I wrote this answer. Testcontainers seem to be a better tool for most projects. The only drawback might be the dependency on Docker. – Mateusz Stefek Nov 30 '19 at 20:50
3

Take a look at this: https://github.com/zonkyio/embedded-database-spring-test. Just to be clear, it's meant for integration testing. Meaning the Spring context is initialised during the individual test.

As per the tools documentation, all you need to do is to place @AutoConfigureEmbeddedDatabase annotation above class:

@RunWith(SpringRunner.class)
@AutoConfigureEmbeddedDatabase
@ContextConfiguration("/path/to/app-config.xml")
public class FlywayMigrationIntegrationTest {

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

and add Maven dependency:

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

To use it together with @DataJpaTest you need to disable the default test database by using the annotation @AutoConfigureTestDatabase(replace = NONE):

@RunWith(SpringRunner.class)
@AutoConfigureTestDatabase(replace = NONE)
@AutoConfigureEmbeddedDatabase
@DataJpaTest
public class SpringDataJpaTest {
// class body...
}

To make the use more comfortable you could also create a composite annotation, something like:

@Documented
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@AutoConfigureTestDatabase(replace = NONE)
@AutoConfigureEmbeddedDatabase
@DataJpaTest
public @interface PostgresDataJpaTest {
}

..and then use it above your test class:

@RunWith(SpringRunner.class)
@PostgresDataJpaTest // custom composite annotation
public class SpringDataJpaTest {
// class body...
}
  • Thanks, I will check that out. However, if it doesn't run with `@DataJpaTest` I will have to find something else. – SingleShot Feb 26 '18 at 00:42
  • @SingleShot I've edited my answer to reflect your comment. We've used this library heavily in my last project and it covered basically all our testing needs. I can highly recommend it. – Martin Volejnik Feb 26 '18 at 18:15
  • Thanks! I'm trying to get it to work but am struggling a bit. I have another wrinkle in that we use Flyway for migrations, which are not running with the above setup. I'll report back if I can figure it out. – SingleShot Feb 26 '18 at 22:36
  • If you have any suggestions I would appreciate them. Thanks! – SingleShot Feb 26 '18 at 23:42
  • @SingleShot Are you using the `@FlywayTest` annotation? You place it either above the test method or test class and you can specify the path to migrations in the annotation. – Martin Volejnik Feb 27 '18 at 06:37
  • @SingleShot We haven't used it with `@DataJpaTest` as we used the full context but I found this: https://github.com/spring-projects/spring-boot/issues/5716 So you might need to add `@ImportAutoConfiguration(FlywayAutoConfiguration.class)` as well. – Martin Volejnik Feb 27 '18 at 07:13
0

You can try https://github.com/TouK/dockds. This auto-configures a docker contained database.

  • Thanks, but that's not at all what I am asking for here. – SingleShot Feb 26 '18 at 22:45
  • Maybe I didn't understand your question. Hmmm... and I still don't. I thought you were for an embedded PostgreSQL configuration for Spring Boot. DockDS is such a thing. It is backed up by Docker, but this is seamless and works well within CI tools like Travis and GitlabCI. The lifecycle of the database instance connected with Spring application context. – Tomasz Wielga Feb 27 '18 at 08:26
  • 2
    Hi Tomasz. By "embedded" I mean it runs inside the Java unit tests. Spring Boot offers 3 embedded databases for unit testing, but I am looking for an alternative that uses Postgres. The goal being anyone can build and test our app without needing to install anything by Java and Maven. Thanks. – SingleShot Feb 27 '18 at 16:57