1. Overview

Writing integration tests with databases offers several options for test databases. One effective option is to use a real database, ensuring that our integration tests closely mimic production behavior.

In this tutorial, we’ll demonstrate how to use Embedded PostgreSQL for Spring Boot tests and review a few alternatives.

2. Dependencies and Configuration

We’ll start by adding the Spring Data JPA dependency, as we’ll use it to create our repositories:

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

To write integration tests for a Spring Boot application, we need to include the Spring Test dependency:

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

Finally, we need to include the Embedded Postgres dependency:

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

Also, let’s set the basic configuration for our integration tests:

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

We’ve specified the PostgreSQLDialect and enabled schema recreation before our test execution.

3. Usage

First things first, let’s create the Person entity that we’ll use in our tests:

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

    @Column
    private String name;

    // getters and setters
}

Now, let’s create a Spring Data Repository for our entity:

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

After that, let’s create a test configuration class:

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

Here, we’ve specified the path to our repository and entity. We’ve created the data source using the EmbeddedPostgres builder, selecting the version of the Postgres database to use during the tests. Additionally, we’ve added the EmbeddedPostgresExtension to ensure that the embedded Postgres connection is closed after executing the test class. Finally, let’s create the test class:

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

We’ve used the @DataJpaTest annotation to set up a basic Spring test context. We’ve extended the test class with our EmbeddedPostgresExtension and attached our EmbeddedPostgresConfiguration to the test context. After that, we successfully created a Person entity and saved it in the database.

4. Flyway Integration

Flyway is a popular migration tool that helps manage schema changes. When we use it, it’s important to include it in our integration tests. In this section, we’ll see how it can be done using the embedded Postgres. Let’s start with the dependencies:

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

After that, let’s specify the database schema in the flyway migration script:

CREATE SEQUENCE IF NOT EXISTS person_seq INCREMENT 50;
;

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

Now we can create the test configuration:

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

We’ve specified the data source bean, where using the PreparedDbProvider and FlywayPreparer we’ve defined the location of the migrations scripts. Finally, here’s our test class:

@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);
    }
}

We’ve disabled the spring.jpa.hibernate.ddl-auto property to allow Flyway to handle schema changes. After that, we saved our Person entity in the database and successfully retrieved it.

5. Alternatives

5.1. Testcontainers

The latest versions of the embedded Postgres project use TestContainers under the hood. Therefore, one alternative is to use the TestContainers library directly. Let’s start by adding the necessary dependencies:

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

Now we’ll create the initializer class, where we configure the PostgreSQLContainer for our tests:

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();
    }
}

We’ve created the PostgreSQLContainer instance and implemented the ApplicationContextInitializer interface to set the configuration properties for our test context. Additionally, we’ve implemented the AfterAllCallback to close the Postgres container connection after the tests. Now, let’s create the test class:

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

Here, we’ve extended the tests by using our TestContainersInitializer and specified the initializer for the test configuration with the @ContextConfiguration annotation. We’ve created the same test cases as in the previous section and successfully saved our Person entity in the Postgres database running in a test container.

5.2. Zonky Embedded Database

Zonky Embedded Database was created as a fork of Embedded Postgres and continues supporting options for a test database without Docker. Let’s add the dependencies that we need to use this library:

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

After that, we’re able to write the test class:

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

Here, we’ve specified the @AutoConfigureEmbeddedDatabase annotation using the ZONKY provider, enabling us to use the embedded Postgres database without Docker. This library also supports other providers such as Embedded and Docker. Finally, we’ve successfully saved our Person entity in the database.

6. Conclusion

In this article, we’ve explored how to use the Embedded Postgres database for testing purposes and reviewed some alternatives. There are various ways to incorporate the Postgres database in tests, both with and without Docker containers. The best choice depends on your specific use case.

As usual, the full source code can be found over on GitHub.