27

I am testing TestContainers and I would like to know how to populate a database executing a .sql file to create the structure and add some rows.

How to do it?

@Rule
public PostgreSQLContainer postgres = new PostgreSQLContainer();
VishnuVS
  • 1,055
  • 2
  • 14
  • 29
jabrena
  • 1,166
  • 3
  • 11
  • 25
  • Are you only using JUnit and TestContainers? Or also other frameworks like Spring Boot for example? – Wim Deblauwe Oct 31 '18 at 07:59
  • Hi, I am using this Library in a Spring Boot environment – jabrena Oct 31 '18 at 15:33
  • 3
    You can find this in the documentation : [Using an init script](https://www.testcontainers.org/usage/database_containers.html#using-an-init-script) – Guillaume Husta Jan 07 '19 at 22:49
  • There is also an article mentioning usage of `PostgreSQLContainer::withInitScript` method: http://muzir.github.io/spring/testing/docker/testcontainers/postgres/2019/03/20/Spring-Boot-Testcontainers.html – luke Jan 02 '21 at 18:14
  • In fact it is `JdbcDatabaseContainer::withInitScript` where `JdbcDatabaseContainer` is superclass of `PostgreSQLContainer` so it should work not only for postgres, but also for other containers. – luke Jan 02 '21 at 18:53

6 Answers6

36

The easiest way is to use JdbcDatabaseContainer::withInitScript

Advantage of this solution is that script is run before Spring Application Context loads (at least when it is in a static block) and the code is quite simple.

Example:

static {
    postgreSQLContainer = new PostgreSQLContainer("postgres:9.6.8")
            .withDatabaseName("integration-tests-db")
            .withUsername("sa")
            .withPassword("sa");
    postgreSQLContainer
            .withInitScript("some/location/on/classpath/someScript.sql");
    postgreSQLContainer.start();
}

JdbcDatabaseContainer is superclass of PostgreSQLContainer so this solution should work not only for postgres, but also for other containers.

If you want to run multiple scripts you can do it in a similar manner

Example:

static {
    postgreSQLContainer = new PostgreSQLContainer("postgres:9.6.8")
            .withDatabaseName("integration-tests-db")
            .withUsername("sa")
            .withPassword("sa");
    postgreSQLContainer.start();

    var containerDelegate = new JdbcDatabaseDelegate(postgreSQLContainer, "");

     ScriptUtils.runInitScript(containerDelegate, "some/location/on/classpath/someScriptFirst.sql");
     ScriptUtils.runInitScript(containerDelegate, "some/location/on/classpath/someScriptSecond.sql");
     ScriptUtils.runInitScript(containerDelegate, "ssome/location/on/classpath/someScriptThird.sql");
}

There are also other options

Spring Test @Sql annotation

@SpringBootTest
@Sql(scripts = ["some/location/on/classpath/someScriptFirst.sql", "some/location/on/classpath/someScriptSecond.sql"])
public class SomeTest {
    //...
}

ResourceDatabasePopulator from jdbc.datasource.init or r2dbc.connection.init when using JDBC or R2DBC consecutively

class DbInitializer {
    private static boolean initialized = false;

    @Autowired
    void initializeDb(ConnectionFactory connectionFactory) {
        if (!initialized) {
            ResourceLoader resourceLoader = new DefaultResourceLoader();
            Resource[] scripts = new Resource[] {
                    resourceLoader.getResource("classpath:some/location/on/classpath/someScriptFirst.sql"),
                    resourceLoader.getResource("classpath:some/location/on/classpath/someScriptSecond.sql"),
                    resourceLoader.getResource("classpath:some/location/on/classpath/someScriptThird.sql")
            };
            new ResourceDatabasePopulator(scripts).populate(connectionFactory).block();
            initialized = true;
        }
    }
}

@SpringBootTest
@Import(DbInitializer.class)
public class SomeTest {
    //...
}

Init script in database URI when using JDBC

It is mentioned in offical Testcontainers documentation:
https://www.testcontainers.org/modules/databases/jdbc/

Classpath file:
jdbc:tc:postgresql:9.6.8:///databasename?TC_INITSCRIPT=somepath/init_mysql.sql

File that is not on classpath, but its path is relative to the working directory, which will usually be the project root:
jdbc:tc:postgresql:9.6.8:///databasename?TC_INITSCRIPT=file:src/main/resources/init_mysql.sql

Using an init function:
jdbc:tc:postgresql:9.6.8:///databasename?TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction

package org.testcontainers.jdbc;

public class JDBCDriverTest {
    public static void sampleInitFunction(Connection connection) throws SQLException {
        // e.g. run schema setup or Flyway/liquibase/etc DB migrations here...
    }
    ...
}
im_infamous
  • 972
  • 1
  • 17
  • 29
luke
  • 3,435
  • 33
  • 41
  • 1
    That solution is great but note it will use the JDBC driver to execute the statements. So it might not work with pgdump output for example. One of https://stackoverflow.com/a/55460428/564005 will – Nicolas Labrot Nov 13 '21 at 13:50
  • Hello, and how if my init_sql file contains @another_sql.sql to execute another sql script ? – taher ben abdallah Mar 29 '22 at 11:32
  • 1
    When using the Spring Test @Sql annotation, the schema will be applied before each test within a transaction. This will slow down the tests. Depending on the size of the schema, the slowdown can be considerable. In one of my tests, applying the schema took almost 2 seconds. If you multiply that with 10 tests, it adds up. – Jipo May 24 '23 at 08:10
10

When using Spring Boot, I find it easiest to use the JDBC URL support of TestContainers.

You can create a application-integration-test.properties file (typically in src/test/resources with something like this:

spring.datasource.url=jdbc:tc:postgresql://localhost/myappdb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=user
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=none
# This line is only needed if you are using flyway for database migrations
# and not using the default location of `db/migration`
spring.flyway.locations=classpath:db/migration/postgresql

Note the :tc part in the JDBC url.

You can now write a unit test like this:

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @ActiveProfiles("integration-test")
public class UserRepositoryIntegrationTest {
      @Autowired
      private MyObjectRepository repository;
      @PersistenceContext
      private EntityManager entityManager;
      @Autowired
      private JdbcTemplate template;

@Test
public void test() {
  // use your Spring Data repository, or the EntityManager or the JdbcTemplate to run your SQL and populate your database.
}

Note: This is explained in Practical Guide to Building an API Back End with Spring Boot, chapter 7 in more detail (Disclaimer: I am the author of the book)

Wim Deblauwe
  • 25,113
  • 20
  • 133
  • 211
  • 2
    Hi Wim, thanks for the reply. Interesting answer, so with TestContainers, it is not necessary to reinvent the wheel, so basically if we have the database then we only need to continue using the solutions from Spring Boot. I will test tomorrow and I will give you the vote :) Congrats with the book reference. I will download tomorrow and I will review. Cheers from Brussels – jabrena Oct 31 '18 at 20:58
  • I am using this but wonder how to configure a fresh container per integration class. – Jibin TJ Nov 08 '18 at 15:22
  • Very helpful answer! – leila-m Jul 10 '23 at 22:50
8

Spring framework provides the ability to execute SQL scripts for test suites or for a test unit. For example:

@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"}) 
public void userTest {
   // execute code that relies on the test schema and test data
}

Here's the documentation.

You can also take a look at Spring Test DBUnit which provides annotations to populate your database for a test unit. It uses XML dataset files.

@Test
@DatabaseSetup(value = "insert.xml")
@DatabaseTearDown(value = "insert.xml")
public void testInsert() throws Exception {
     // Inserts "insert.xml" before test execution
     // Remove "insert.xml" after test execution
}

Also, you can take a look at DbSetup, which provides a java fluent DSL to populate your database.

victor gallet
  • 1,819
  • 17
  • 25
  • How do you add SQL like this when working on a complex database and need to maintain the foreign keys relationships? – Arefe Jan 14 '21 at 11:34
6

There is one more option, if you are defining Postgres container manually without fancy testcontainers JDBC url stuff, not related to Spring directly. Postgres image allows to link directory containing sql scripts to container volume and auto-executes them.

GenericContainer pgDb = new PostgreSQLContainer("postgres:9.4-alpine")
  .withFileSystemBind("migrations/sqls", "/docker-entrypoint-initdb.d",
    BindMode.READ_ONLY)

Also if you need something in runtime, you can always do pgDb.execInContainer("psql ....").

glebsts
  • 337
  • 4
  • 11
5

You can use DatabaseRider, which uses DBUnit behind the scenes, for populating test database and TestContainers as the test datasource. Following is a sample test, full source code is available on github here.

@RunWith(SpringRunner.class)
@SpringBootTest
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @ActiveProfiles("integration-test")
@DBRider //enables database rider in spring tests 
@DBUnit(caseInsensitiveStrategy = Orthography.LOWERCASE) //https://stackoverflow.com/questions/43111996/why-postgresql-does-not-like-uppercase-table-names
public class SpringBootDBUnitIt {

    private static final PostgreSQLContainer postgres = new PostgreSQLContainer(); //creates the database for all tests on this file 

    @PersistenceContext
    private EntityManager entityManager;

    @Autowired
    private UserRepository userRepository;


    @BeforeClass
    public static void setupContainer() {
        postgres.start();
    }

    @AfterClass
    public static void shutdown() {
        postgres.stop();
    }


    @Test
    @DataSet("users.yml")
    public void shouldListUsers() throws Exception {
        assertThat(userRepository).isNotNull();
        assertThat(userRepository.count()).isEqualTo(3);
        assertThat(userRepository.findByEmail("springboot@gmail.com")).isEqualTo(new User(3));
    }

    @Test
    @DataSet("users.yml") //users table will be cleaned before the test because default seeding strategy
    @ExpectedDataSet("expected_users.yml")
    public void shouldDeleteUser() throws Exception {
        assertThat(userRepository).isNotNull();
        assertThat(userRepository.count()).isEqualTo(3);
        userRepository.delete(userRepository.findOne(2L));
        entityManager.flush();//can't SpringBoot autoconfigure flushmode as commit/always
        //assertThat(userRepository.count()).isEqualTo(2); //assertion is made by @ExpectedDataset
    }

    @Test
    @DataSet(cleanBefore = true)//as we didn't declared a dataset DBUnit wont clear the table
    @ExpectedDataSet("user.yml")
    public void shouldInsertUser() throws Exception {
        assertThat(userRepository).isNotNull();
        assertThat(userRepository.count()).isEqualTo(0);
        userRepository.save(new User("newUser@gmail.com", "new user"));
        entityManager.flush();//can't SpringBoot autoconfigure flushmode as commit/always
        //assertThat(userRepository.count()).isEqualTo(1); //assertion is made by @ExpectedDataset
    }

}

src/test/resources/application-integration-test.properties

spring.datasource.url=jdbc:tc:postgresql://localhost/test
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=test
spring.datasource.password=test
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
#spring.jpa.properties.org.hibernate.flushMode=ALWAYS #doesn't take effect 
spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

And finally the datasets:

src/test/resources/datasets/users.yml

users:
  - ID: 1
    EMAIL: "dbunit@gmail.com"
    NAME: "dbunit"
  - ID: 2
    EMAIL: "rmpestano@gmail.com"
    NAME: "rmpestano"
  - ID: 3
    EMAIL: "springboot@gmail.com"
    NAME: "springboot"

src/test/resources/datasets/expected_users.yml

users:
  - ID: 1
    EMAIL: "dbunit@gmail.com"
    NAME: "dbunit"
  - ID: 3
    EMAIL: "springboot@gmail.com"
    NAME: "springboot"

src/test/resources/datasets/user.yml

users:
  - ID: "regex:\\d+"
    EMAIL: "newUser@gmail.com"
    NAME: "new user"
rmpestano
  • 838
  • 1
  • 8
  • 17
-4

After some reviews, I think that it is interesting to review the examples from Spring Data JDBC which use Test Containers:

Note: Use Java 8

git clone https://github.com/spring-projects/spring-data-jdbc.git
mvn clean install -Pall-dbs

I will create a simple project adding some ideas about previous project referenced.

Juan Antonio

jabrena
  • 1,166
  • 3
  • 11
  • 25