Singleton testcontainers for Integration tests
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 !!!