TestContainers: The modern way of writing database tests

Hiranmaya Gundu
MiQ Tech and Analytics
4 min readJul 17, 2020

TestContainers is an open-source project that allows you to run docker containers directly in your project. You can check out the project here. It has bindings in Java, Python, Rust, Go, Scala, and many more languages. While this is definitely cool, you may ask what it has to do with database testing?

When testing Java code that interacts directly with your database, it is common practice to let the test cases target in-memory databases like H2 or Fongo (an in-memory MongoDB). This ensures that we do not mock the database calls. The H2 database is maintained by the test cases. The lack of the need to maintain the database is a huge win. There is no need to worry about keeping the port open, bring up a clean instance, spin it down etc.

However, we write automated test cases to build confidence in our code base. Test cases help us catch bugs and rectify them before we push it to production. The golden rule of testing is: “The more your tests resemble the way your software is used, the more confidence they can give you”. Our clients do not use H2. These test cases do not guarantee that our code will work with Postgres or Mongo, as those databases may react differently to the same code.

The drawbacks do not stop there. Say we are migrating from one major version of PostgresDB to another. Major semantic-version changes generally do not ensure backwards compatibility. Test cases written to target H2 will not catch any bugs that occur because of any breaking/compatibility issues between the two versions. This would force us to write tests against PostgresDB anyway.

Another issue, and the issue we faced here at MiQ, is that H2 may not have the features that your production DB has. PostgresDB has had JSONB since the end of 2018, while it took H2 almost a year to add the same feature. Our choices when testing against H2 are limited. We can either not write tests for them, or not use that feature at all. If we do not use that feature, a testing choice dictates production technological choices. This prompted our search for an alternative.

So why is H2 so popular? The overhead of maintaining a test DB outweighs the drawbacks of using H2 as your test DB. But, this difficulty can be solved using docker containers. You can spin up a clean database when you need it and then spin it down and destroy it when you don’t. We can, of course, integrate this into our build steps ourselves. But we want a solution where the DB is maintained by the test cases.

This is where TestContainers comes in. TestContainers is an open-source project that provides lightweight, throwaway instances of anything that can run in a Docker container. Spinning up a MySQL database is as simple as adding 3 lines of code:-

class SimpleMySQLTest {
private MySQLContainer mysql = new MySQLContainer();

@Before
void before() {
mysql.start();
// You can use mysql.getJdbcUrl(), mysql.getUsername() and
// mysql.getPassword() to connect to the container

}

@After
void after() {
mysql.stop();
}

// [...]
}

TestContainers provides @Rule/ @ClassRule integration to allow JUnit 4 to control the lifecycle of the container. It also provides an @Container annotation for Junit5. These utilities reduce it to just one line of code. You can look at the examples here.

Lifecycle of a test when using @Rule/@ClassRule

HibernateORM is the most popular choice for interacting with databases in Java. To connect your Hibernate classes to TestContainers, it is as simple as this:

class SimpleHibernateTest {

@Rule
private MySQLContainer mysql = new MySQLContainer().withDatabaseName("local").withUsername("USER").withPassword("PWD");

@Before
public void before() {
final Configuration configuration = new Configuration();
// Base configuration
configuration.configure("hibernate.cfg.xml")
final Properties properties = new Properties();
properties.setProperty("hibernate.hikari.dataSource.url", mysql.getJdbcUrl());
properties.setProperty("hibernate.hikari.datasource.user", "USER");
properties.setProperty("hibernate.hikari.datasource.password", "PWD");
configuration.addProperties(properties);
configuration.addAnnotatedClass(ClassToTest.class);
}

@Test
public void test() {
// write test case here
}
}

So, to migrate our existing H2 tests to TestContainers based Postgres tests, we made the following changes:-

  1. We removed H2 related properties from hibernate.cfg.xml and added Postgres related ones.
// remove this 
<property name=”hibernate.connection.driver_class”>org.h2.Driver</property>
<property name=”hibernate.connection.url”>
jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL;INIT=CREATE domain IF NOT EXISTS jsonb AS
other;
</property>
<property name=”hibernate.dialect”>org.hibernate.dialect.H2Dialect</property>
// add this
<property name=”hibernate.hikari.dataSourceClassName”>org.postgresql.ds.PGSimpleDataSource
</property>

2. Add the @Before block and @Rule code as described above

And that’s it! Our test cases are now migrated.

LIMITATIONS:

There are of course drawbacks to using TestContainers. Test cases will be much slower compared to H2. There are ways to mitigate this. The @Rule annotation brings up a new database for every test case in your class. Bringing up a database is a costly operation, and we can optimize this by using @ClassRule. When you use @ClassRule, one database is brought up for all the tests in the class. In this scenario, it becomes important to ensure that you are cleaning up the data in the database after a test case runs, to ensure test isolation. This can be done by using the @After lifecycle.

Another drawback is that the test cases can fail if it is unable to download the docker image. These drawbacks are minor, however, compared to the benefits of using TestContainers.

--

--