Integrating TestContainers With JUnit 5

After we have finished this webinar, we

  • Understand how we can integrate TestContainers with JUnit 5.
  • Know how we can create a JUnit 5 extension that integrates TestContainers with JUnit 5.
  • Can use our custom extension when we write tests with JUnit 5.

Get the source code from Github

Using the Manual Approach

As we remember , when we use TestContainers with JUnit 4, we can start and stop our Docker containers by using JUnit 4 rules. If we want to use TestContainers with JUnit 5, we can start and stop our Docker containers by using setup and teardown methods.

Note that TestContainers 2.0 will have built-in support for JUnit 5.

Let’s write a test class which uses a Docker container that runs a PostgreSQL database. We can write this test class by following these steps:

First, we have add a private and static PostgreSQLContainer field to our test class and ensure that this field contains a reference to a new PostgreSQLContainer object.

After we have added this field to our test class, its source code looks as follows:

1import org.junit.jupiter.api.DisplayName;
2import org.testcontainers.containers.PostgreSQLContainer;
3
4@DisplayName("Run a PostgreSQL container")
5class PostgreSQLContainerTest {
6
7    private static PostgreSQLContainer postgreSQL = new PostgreSQLContainer();
8}

Second, we have to add a setup method to our test class and ensure that this method is run before any test method is run. After we have added this method to our test class, we have to implement it by starting our Docker container.

After we have written this setup method, the source code of our test class looks as follows:

 1import org.junit.jupiter.api.BeforeAll;
 2import org.junit.jupiter.api.DisplayName;
 3import org.testcontainers.containers.PostgreSQLContainer;
 4
 5@DisplayName("Run a PostgreSQL container")
 6class PostgreSQLContainerTest {
 7
 8    private static PostgreSQLContainer postgreSQL = new PostgreSQLContainer();
 9
10    @BeforeAll
11    static void startDatabase() {
12        postgreSQL.start();
13    }
14}

Third, we have to write the test method which ensures that the PostgreSQL database was started successfully. After we have written this test method, the source code of our test class looks as follows:

 1import org.junit.jupiter.api.BeforeAll;
 2import org.junit.jupiter.api.DisplayName;
 3import org.junit.jupiter.api.Test;
 4import org.testcontainers.containers.PostgreSQLContainer;
 5
 6import java.sql.Connection;
 7import java.sql.DriverManager;
 8import java.sql.ResultSet;
 9import java.sql.SQLException;
10
11import static org.assertj.core.api.Assertions.assertThat;
12
13@DisplayName("Run a PostgreSQL container")
14class PostgreSQLContainerTest {
15
16    private static PostgreSQLContainer postgreSQL = new PostgreSQLContainer();
17
18    @BeforeAll
19    static void startDatabase() {
20        postgreSQL.start();
21    }
22
23    @Test
24    @DisplayName("Should run a PostgreSQL container")
25    void shouldRunPostgreSQLContainer() throws SQLException {
26        Connection conn = DriverManager.getConnection(
27                postgreSQL.getJdbcUrl(),
28                postgreSQL.getUsername(),
29                postgreSQL.getPassword()
30        );
31
32        ResultSet resultSet = conn.createStatement()
33                .executeQuery("SELECT 42");
34        resultSet.next();
35
36        int result = resultSet.getInt(1);
37        assertThat(result).isEqualByComparingTo(42);
38    }
39}

Fourth, we have to add a teardown method to our test class and ensure that this method is run after all test methods have been run. After we have added this method to our test class, we have to implement it by stopping our Docker container.

After we have written this teardown method, the source code of our test class looks as follows:

 1import org.junit.jupiter.api.AfterAll;
 2import org.junit.jupiter.api.BeforeAll;
 3import org.junit.jupiter.api.DisplayName;
 4import org.junit.jupiter.api.Test;
 5import org.testcontainers.containers.PostgreSQLContainer;
 6
 7import java.sql.Connection;
 8import java.sql.DriverManager;
 9import java.sql.ResultSet;
10import java.sql.SQLException;
11
12import static org.assertj.core.api.Assertions.assertThat;
13
14@DisplayName("Run a PostgreSQL container")
15class PostgreSQLContainerTest {
16
17    private static PostgreSQLContainer postgreSQL = new PostgreSQLContainer();
18
19    @BeforeAll
20    static void startDatabase() {
21        postgreSQL.start();
22    }
23
24    @Test
25    @DisplayName("Should run a PostgreSQL container")
26    void shouldRunPostgreSQLContainer() throws SQLException {
27        Connection conn = DriverManager.getConnection(
28                postgreSQL.getJdbcUrl(),
29                postgreSQL.getUsername(),
30                postgreSQL.getPassword()
31        );
32
33        ResultSet resultSet = conn.createStatement()
34                .executeQuery("SELECT 42");
35        resultSet.next();
36
37        int result = resultSet.getInt(1);
38        assertThat(result).isEqualByComparingTo(42);
39    }
40
41    @AfterAll
42    static void stopDatabase() {
43        postgreSQL.stop();
44    }
45}

When we run our test, we see that it passes.

Even though this example uses a Docker container that starts a PostgreSQL database, the process is same for all supported container types.

Additional Reading:

We can now start and stop our Docker container by using the manual approach. The downside of this approach is that we have to add duplicate code to our test classes. Next, we will find out how we can get rid off this code by writing a custom JUnit 5 extension.

Writing a Custom JUnit 5 Extension

If we want to get rid off duplicate code, we have to write a custom JUnit 5 extension that starts and stops our Docker container. The requirements of our custom extension are:

  • It must get the started Docker container object from a private and static field that is found from the test class which uses our custom extension.
  • It must start our Docker container before any test method is run.
  • It must stop our Docker container after all test methods have been run.

We can write this extension by following these steps:

First, we must create a new class that implements the BeforeAllCallback and AfterAllCallback interfaces.

The source code of the created class looks as follows:

 1import org.junit.jupiter.api.extension.AfterAllCallback;
 2import org.junit.jupiter.api.extension.BeforeAllCallback;
 3import org.junit.jupiter.api.extension.ExtensionContext;
 4
 5public class TestContainersExtension implements BeforeAllCallback, AfterAllCallback {
 6
 7    @Override
 8    public void afterAll(ExtensionContext extensionContext) throws Exception {
 9
10    }
11
12    @Override
13    public void beforeAll(ExtensionContext extensionContext) throws Exception {
14
15    }
16}

Second, we have to write a private getContainer() method that returns a reference to the started GenericContainer object. This method takes a Class object as a method parameter and finds the required GenericContainer object by using reflection.

After we have implemented this method, the source code of our extension class looks as follows:

 1import org.junit.jupiter.api.extension.AfterAllCallback;
 2import org.junit.jupiter.api.extension.BeforeAllCallback;
 3import org.junit.jupiter.api.extension.ExtensionContext;
 4import org.junit.platform.commons.util.PreconditionViolationException;
 5import org.testcontainers.containers.GenericContainer;
 6
 7import java.lang.reflect.Field;
 8import java.lang.reflect.Modifier;
 9import java.util.Arrays;
10
11public class TestContainersExtension implements BeforeAllCallback, AfterAllCallback {
12
13    @Override
14    public void afterAll(ExtensionContext extensionContext) throws Exception {
15
16    }
17
18    @Override
19    public void beforeAll(ExtensionContext extensionContext) throws Exception {
20
21    }
22
23    private GenericContainer getContainer(Class<?> testClass)
24            throws IllegalAccessException {
25        Field testContainerField = Arrays.stream(testClass.getDeclaredFields())
26                .filter(this::isContainerField)
27                .findFirst()
28                .orElseThrow(() -> new PreconditionViolationException(""));
29
30        testContainerField.setAccessible(true);
31
32        //We can pass null to this method because the field must be static
33        return (GenericContainer) testContainerField.get(null);
34    }
35
36    private boolean isContainerField(Field f) {
37        return Modifier.isStatic(f.getModifiers())
38                && GenericContainer.class.isAssignableFrom(f.getType());
39    }
40}

Third, we have start our Docker container before any test method is run and stop it after all test methods have been run. After we have done this, the source code of our extension class looks as follows:

 1import org.junit.jupiter.api.extension.AfterAllCallback;
 2import org.junit.jupiter.api.extension.BeforeAllCallback;
 3import org.junit.jupiter.api.extension.ExtensionContext;
 4import org.junit.platform.commons.util.PreconditionViolationException;
 5import org.testcontainers.containers.GenericContainer;
 6
 7import java.lang.reflect.Field;
 8import java.lang.reflect.Modifier;
 9import java.util.Arrays;
10
11public class TestContainersExtension implements BeforeAllCallback, AfterAllCallback {
12
13    @Override
14    public void afterAll(ExtensionContext extensionContext) throws Exception {
15        GenericContainer container = getContainer(extensionContext.getRequiredTestClass());
16        container.stop();
17    }
18
19    @Override
20    public void beforeAll(ExtensionContext extensionContext) throws Exception {
21        GenericContainer container = getContainer(extensionContext.getRequiredTestClass());
22        container.start();
23    }
24
25    private GenericContainer getContainer(Class<?> testClass)
26            throws IllegalAccessException {
27        Field testContainerField = Arrays.stream(testClass.getDeclaredFields())
28                .filter(this::isContainerField)
29                .findFirst()
30                .orElseThrow(() -> new PreconditionViolationException(""));
31
32        testContainerField.setAccessible(true);
33
34        //We can pass null to this method because the field must be static
35        return (GenericContainer) testContainerField.get(null);
36    }
37
38    private boolean isContainerField(Field f) {
39        return Modifier.isStatic(f.getModifiers())
40                && GenericContainer.class.isAssignableFrom(f.getType());
41    }
42}

We have now written a custom JUnit 5 extension that can start and stop our Docker container. Let’s move on and find out how we can use our new custom extension.

Using Our Custom Extension

When we want write a test class which uses a Docker container that runs a PostgreSQL database, we have to follow these steps:

First, we have to register our custom extension. We can do this by annotating our test class with the @ExtendWith annotation.

After we have registered our custom extension, the source code of our test class looks as follows:

1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.api.extension.ExtendWith;
3
4@ExtendWith(TestContainersExtension.class)
5@DisplayName("Run a PostgreSQL container")
6class PostgreSQLContainerExtensionTest {
7
8}

Second, we have add a private and static PostgreSQLContainer field to our test class and ensure that this field contains a reference to a new PostgreSQLContainer object.

After we have added this field to our test class, its source code looks as follows:

 1import org.junit.jupiter.api.DisplayName;
 2import org.junit.jupiter.api.extension.ExtendWith;
 3import org.testcontainers.containers.PostgreSQLContainer;
 4
 5@ExtendWith(TestContainersExtension.class)
 6@DisplayName("Run a PostgreSQL container")
 7class PostgreSQLContainerExtensionTest {
 8
 9    private static PostgreSQLContainer postgreSQL = new PostgreSQLContainer();
10}

Third, we have to write the test method which ensures that the PostgreSQL database was started successfully. After we have written this test method, the source code of our test class looks as follows:

 1import org.junit.jupiter.api.DisplayName;
 2import org.junit.jupiter.api.Test;
 3import org.junit.jupiter.api.extension.ExtendWith;
 4import org.testcontainers.containers.PostgreSQLContainer;
 5
 6import java.sql.Connection;
 7import java.sql.DriverManager;
 8import java.sql.ResultSet;
 9import java.sql.SQLException;
10
11import static org.assertj.core.api.Assertions.assertThat;
12
13@ExtendWith(TestContainersExtension.class)
14@DisplayName("Run a PostgreSQL container")
15class PostgreSQLContainerExtensionTest {
16
17    private static PostgreSQLContainer postgreSQL = new PostgreSQLContainer();
18
19    @Test
20    @DisplayName("Should run a PostgreSQL container")
21    void shouldRunPostgreSQLContainer() throws SQLException {
22        Connection conn = DriverManager.getConnection(
23                postgreSQL.getJdbcUrl(),
24                postgreSQL.getUsername(),
25                postgreSQL.getPassword()
26        );
27
28        ResultSet resultSet = conn.createStatement()
29                .executeQuery("SELECT 42");
30        resultSet.next();
31
32        int result = resultSet.getInt(1);
33        assertThat(result).isEqualByComparingTo(42);
34    }
35}

When we run our test, we see that it passes.

Again, even though this example uses a Docker container that starts a PostgreSQL database, the process is same for all supported container types.

We can start and stop Docker containers with JUnit 5. Let’s summarize what we learned from this webinar.

Summary

This webinar has taught us two things:

  • We can start and stop our Docker container by using setup and teardown methods.
  • If we want to remove duplicate code from our test classes, we can implement a custom JUnit 5 extension that starts and stops our Docker container.

Get the source code from Github