When writing a test with Testcontainers and Spring, the pattern is to use @DynamicPropertySource
to register the container resource properties.
@SpringJUnitConfig(...)
@Testcontainers
class ExampleIntegrationTests {
@Container
static RedisContainer redis = new RedisContainer();
// ...
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("redis.host", redis::getHost);
registry.add("redis.port", redis::getMappedPort);
}
}
(Copied from @DynamicPropertySource
javadoc)
This is ok for a single test class. However, while writing multiple such integration tests, in order to share the same containers, usually people create a parent base class. One reason is that @DynamicPropertySource
needs to be in the test class hierarchy to register the resource info from the testcontainers. This enforces the inheritance pattern.
With this pattern, I often see that the parent class has many containers but individual child test classes only require a subset of containers.
class IntegrationTestsBase {
@Container
static RedisContainer redis = new RedisContainer();
@Container
static MySQLContainer mysql = new MySQLContainer();
@Container
static MyContainer custom = new MyContainer();
@DynamicPropertySource
static void register(DynamicPropertyRegistry registry) {
...
}
}
class OnlyNeedRedisTests extends IntegrationTestsBase {
// only use redis container
}
class OnlyNeedMySqlTests extends IntegrationTestsBase {
// only use mysql container
}
Rather than inheritance, I prefer to have an annotation for each container resource.
For example, I wrote something like this to use either locally running mysql or testcontainer.
public class MyDbTestExtension implements BeforeAllCallback, AfterAllCallback {
static final MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:8");
static final String localJdbcUrl = "jdbc:mysql://localhost:3306/my_db?user=root&password=password";
private static final String SPRING_DB_PROPERTY = "spring.datasource.url";
@Override
public void beforeAll(ExtensionContext context) {
try {
// check local mysql is running or not
DriverManager.getConnection(localJdbcUrl);
}
catch (Exception ex) {
mySQLContainer.start();
}
// To encapsulate logic in extension, use system property to let spring pick up
// the DB url. @DynamicPropertySource unfortunately only works within test class.
System.setProperty(SPRING_DB_PROPERTY, getJdbcUrl());
}
public static String getJdbcUrl() {
if (mySQLContainer.isRunning()) {
return mySQLContainer.getJdbcUrl();
}
return localJdbcUrl;
}
@Override
public void afterAll(ExtensionContext context) throws Exception {
System.clearProperty(SPRING_DB_PROPERTY);
}
}
In my Boot test, I simply extend it without parent class.
@DataJpaTest(...)
@ExtendWith(MyDbTestExtension.class)
class MyDbTests {
}
One problem of the above junit extension is that it is using System.setProperty
to pass the testcontainer resource information to the Spring properties. It would be a problem to run such tests in parallel.
If there is a simple solution to pass the Spring property from JUnit extension to the test context, it could avoid using system properties.
One quick idea is that if there is a particular variable in ExtensionContext.Store
that spring understands, it could be used to pass property key-values from an extension to the spring test context infrastructure.
Comment From: preceda
I'm looking for exactly the same solution. It would be really nice to have it.
Comment From: ttddyy
FYI, for testcontainers integration, I wrote a PoC with a different approach that uses ContextCustomizer
to populate properties.(Kind of a variant of @DynamicPropertySource
)
https://github.com/ttddyy/demo-testcontainer-integration
Comment From: vpavic
Since both @Container
field and @DynamicPropertySource
method need to be static
, couldn't you also use something along these lines:
@Testcontainers
@DirtiesContext
interface PostgreSqlIntegrationTest {
@Container
JdbcDatabaseContainer<?> postgresql = new PostgreSQLContainerProvider().newInstance("14.6-alpine");
@DynamicPropertySource
static void dataSourceProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgresql::getJdbcUrl);
registry.add("spring.datasource.username", postgresql::getUsername);
registry.add("spring.datasource.password", postgresql::getPassword);
}
}
And then provide the equivalent interfaces for other containers you use within your project.
This removes the need to have a base integration test class with all the containers, as each integration test class can pick which ones to use by implementing the appropriate interface.
Comment From: snicoll
The support for ServiceConnection in Spring Boot has to a large extent made this request obsolete, at least in its original form. I don't really see how a JUnit extension could see the Spring environment, that looks a bit backwards to me.
However, manipulating the environment is possible, see SpringBootTestContextBootstrapper#processPropertySourceProperties
for example.