2 min read

Singleton testcontainers for Integration tests

Reuse containers during integration test, save time between startup and shutdown of each container
Singleton testcontainers for Integration tests
Photo by Roman Mager / Unsplash

We've all been in that situation where we needed to write integration tests against some service, to ensure it works exactly as expected. Case in point, say your application is using Postgres, and you use H2 for tests, and it's available at runtime. You'd run into issues where the queries you've written against Postgres are unable to run against the H2 Database. To solve this problem, Developers used docker services to startup containers just for their tests. Again, the trouble with this approach is that is is not portable. You needed to run some bash scripts on your CI pipeline to ensure that the Docker container is started. Every other developer had to know how exactly to run the tests.

Testcontainers to the rescue. This is a Java library which helps you manage Docker containers for testing. It basically creates so-called throwaway containers for your tests to run, and destroys them once done. And this is exactly what we want during integration tests. Here is a sample code using Testcontainers in a Spring boot application.

Singleton TestContainers

When running multiple tests, you might run into the following error log.

Connection to localhost:55607 refused. Check that the hostname and port are correct

This could be because the container doesn't start at all. But if the container starts for a single test, but fails when you run all tests, the simple explanation is that: for each test a "throwaway" container, in this case: a database container is created and destroyed after the test completes. Also, "random" host ports are supposed to be allocated to the Database container. But there is no guarantee that this would happen. The Test process may also run into issues waiting to acquire a connection. This can be solved by using Singleton containers. This simply means sharing containers between tests.

So rather than destroy the containers after each. Such an expensive container can be used to carry out all the tests, and thereafter destroyed.

@SpringBootTest
@ActiveProfiles("integration-test")
@Testcontainers
@AutoConfigureMockMvc
public abstract class AbstractIntegrationTest {

    protected static final ObjectMapper objectMapper = new ObjectMapper();
    protected static final UUID CARD_TEMPLATE_01 = UUID.fromString("00000000-0000-0000-0000-000000000001");
    private static final PostgreSQLContainer postgreSQLContainer;
    private static final KeycloakContainer keyCloakContainer;

    @Autowired
    protected MockMvc mockMvc;

    static {
        objectMapper.findAndRegisterModules();
        postgreSQLContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
        keyCloakContainer = new KeycloakContainer().withRealmImportFile("/imports/test-realm-export.json");
        postgreSQLContainer.start();//singleton container started once in this class and used by all inheriting test classes
        keyCloakContainer.start();
    }

    @DynamicPropertySource
    private static void overrideProps(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
        registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
        registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
    }
}

Line 17 creates the PostgreSQLContainer once in this class. The container is also started in line 20. Note that this is an abstract class used to define common test resources. Every Integration test would inherit this class. CardControllerTest is a concrete class which extends the AbstractIntegrationTest. So by the time the CardControllerTest is run, the 2 Containers: Keycloak and Postgres would already be started, and they won't be shutdown, until all tests have completed, then they would automatically be shutdown afterwards.

class CardControllerTest extends AbstractIntegrationTest {

    @Autowired
    CardTemplateDataDatabaseAdapter cardDatabaseAdapter;

    @Test
    @WithMockUser(authorities = {"ADMIN", "USER"})
    void testCardCreation() throws Exception {

        CardTemplate cardTemplate = cardDatabaseAdapter.findById(CARD_TEMPLATE_01).orElseThrow();

        CardCreationRequestUIO cardCreationRequestUIO = ModelFactoryRegistry.make(CardCreationRequestUIO.class)
                .withTemplateName(cardTemplate.getName())
                .withAnniversary(MonthDay.of(Month.AUGUST, 15));

        mockMvc.perform(post("/v1/cards").contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(cardCreationRequestUIO)))
                .andExpect(status().isOk());
    }

}

Happy Coding !!!