Testcontainers#
TLDR#
By using @TestExecutionListeners(mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
and importing PsqlTestContainerConfiguration in your Spring context on Spring boot managed tests, the following behavior
are configured automatically :
A postgresql container using a dynamic port is started with tests
Spring datasource is automatically reconfigured to use the postgresql container
Flyway migration is performed at spring startup time
Before
@Sqlspring handling, the following tasks are performedDatabase cleaning by truncating tables (PostgreSQL only) ; flyway migration table is kept untouched
Second level cache cleaning
Hibernate search index cleaning
After
@Sqlspring handling, the following tasks are performedHibernate search reindexation
All the configuration are managed by spring autoconfigure, and only available and configured items are triggered.
If @TestInstance(TestInstance.Lifecycle.PER_CLASS) is used, cleaning/reindexing is performed on class
lifecycle.
Configurations#
It is possible to customize some behavior :
igloo.test.listener.cache-level2.enabled=falseto disable level 2 cache handlingigloo.test.listener.hsearch.enabled=falseto disable hibernate-search handlingigloo.test.listener.psql.enabled=falseto disable postgresql handlingigloo.test.listener.psql.excludes=*.table,schema.*,schema.tableto exclude tables from cleaningigloo.test.listener.psql.=*.table,schema.*,schema.tableto exclude tables from cleaningigloo.test.listener.psql.exclude-flyway-table=falsenot to exclude flyway migration table
Default configuration is to enable all available and configured cleans, and to exclude flyway migration table from cleaning.
How does it work#
The setup is based on the following items :
IglooTestExecutionListenerandAfterSqlIglooTestExecutionListenerauto-registeredTestExecutionListener(withMETA-INF/spring.factoriesconfiguration file)IIglooTestListenerbeans. A name matching is used to bind eachIIglooTestListenerto the appropriateTestExecutionListener(eitherdefaultorafter-sqlfor the auto-configured ones)*IglooTestListenerAutoConfiguration, auto-registered withMETA-INF/spring/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsto instantiate conditionaly the appropriateIIglooTestListenerbased on classpath and bean conditions.
HsearchUtil and PsqlUtil performs the actual cleaning tasks.
Troubleshooting#
If you encounter some issues, you may check the following items :
If datasource is not bind to datasource, you may check that (with breakpoint on constructor) :
JdbcContainerConnectionDetailsis createdNo
PropertiesJdbcConnectionDetailsis createdDataSourceAutoConfigurationis loadedServiceConnectionAutoConfigurationis loadedYou may need to add
HikariCPdependency to triggerDataSourceAutoConfiguration
Ensure that
IglooTestExecutionListenerandAfterSqlIglooTestExecutionListenerare created. If not, check the@TestExecutionListener#mergeModesetting, or register manually these listenersIf you migrate to testcontainers, and tests complains about a missing
EntityManager, check that you keepEntityManagerExecutionListenerwhen you migrate@TestExecutionListenersIf
@Sqlare not applied, check thatSqlScriptsTestExecutionListeneris created. If not, the@TestExecutionListener#mergeModesetting.You can activate
igloo.test.listenerlogger at info or debug level to check which listeners are triggeredYou can place breakpoints in
IIglooTestListenersubclasses to check if and when callbacks are triggeredYou can place a breakpoint and use
docker psto check the container port and connect withpsql -U USER -p PORT -h localhost
Power-user#
It is possible to declare new IIglooTestListener. You need to choose an appropriate name to bind to the
appropriate IglooTestExecutionListener or AfterSqlIglooTestExecutionListener.
If you bind to IglooTestExecutionListener or AfterSqlIglooTestExecutionListener, before and after methods
are called on before/after test method or class, based on @TestInstance setting.
You can subclass one of IglooTestExecutionListener subclasses if you want to handle before / after binding
differently. If so, declare new IIglooTestListener with a customized name matching. There is subclass for the
following use-cases :
IglooAnyTestExecutionListener : each step call
IIglooTestListener; appropriate handling can be performed with theIglooTestListenerTypeargument.IglooClassTestExecutionListener :
IIglooTestListeneris invoked only for before / after class (ignoring@TestInstance)IglooMethodTestExecutionListener :
IIglooTestListeneris invoked only for before / after method (ignoring@TestInstance)IglooExecutionTestExecutionListener :
IIglooTestListeneris invoked only for before / after execution (ignoring@TestInstance)
If you use a custom IglooExecutionTestExecutionListener, it is up to you to register it (@TestExecutionListeners
annotation or META-INF/spring.factories).
Testcontainers migration#
(gitlab docker runner) CI/CD must enable docker:dind
Use
mergeMode = MergeMode.MERGE_WITH_DEFAULTSon@TestExecutionListenersAdd (or merge)
@Import({PsqlTestContainerConfiguration.class})in your test spring configurationReplace in
configuration-env-test.propertiesdatabase related properties (see snippet below)Shutdown your postgresql test instance
These steps should be enough to run your tests successfully
Optional : remove database cleaning code (IglooTestExecutionListener default setup handles database, second level cache, and database cleaning)
remove calls to
AbstractTestCase#init()remove dead code
cleanAll,cleanReferenceData
Check again test execution
Optional : translate existing initialization to SQL
Optional : add
@SqlMergeMode(SqlMergeMode.MergeMode.MERGE)to allow common + specialized initialization (see snippet below)Optional : add
@Sql(scripts = "/scripts/init-data-test.sql")and a init file insrc/test/resources/scripts
@SpringBootTest.. annotation or ApplicationCoreTestCommonConfig
@Import({ApplicationCoreCommonConfiguration.class, PsqlTestContainerConfiguration.class})
TestExecutionListeners configuration
Search TestExecutionListeners occurrences.
Spring listeners can be removed; they are loaded by MERGE_WITH_DEFAULTS.
EntityManagerExecutionListener must be kept if already present.
@TestExecutionListeners(
listeners = EntityManagerExecutionListener.class,
mergeMode =
TestExecutionListeners.MergeMode
.MERGE_WITH_DEFAULTS // Retains default TestExecutionListeners.
)
configuration-env-test.properties
Remove spring.datasource.* setup, and add PsqlTestContainerConfiguration configurations.
# REMOVE spring database-related properties
spring.datasource.url=jdbc:postgresql://${TEST_DB_HOST:localhost}:${TEST_DB_PORT:5436}/${TEST_DB_NAME:basic_application_test}
spring.datasource.username=${TEST_DB_USER:basic_application_test}
spring.datasource.password=${TEST_DB_PASSWORD:basic_application_test}
spring.jpa.properties.hibernate.default_schema=${TEST_DB_USER:basic_application_test}
spring.flyway.defaultSchema=${TEST_DB_USER:basic_application_test}
db.schema=${TEST_DB_USER:basic_application_test}
# ADD testcontainers and static schema definition properties
spring.jpa.properties.hibernate.default_schema=basic_application_test
spring.flyway.defaultSchema=basic_application_test
db.schema=basic_application_test
spring.flyway.clean-disabled=false
testContainer.database.name=basic_application_test
testContainer.database.userName=basic_application_test
testContainer.database.password=basic_application_test
testContainer.database.exposedPorts=5432
testContainer.database.dockerImageName=postgres:17-alpine
Remove obsolete cleaning code (back and front)
// [...]
public class AbstractApplicationTestCase extends AbstractTestCase {
// [...]
@BeforeEach
@Override
public void init() throws ServiceException, SecurityServiceException {
// Database cleaning performed by CleanDatabaseTestExecutionListener
}
@AfterEach
@Override
public void close() throws ServiceException, SecurityServiceException {
// Database cleaning performed by CleanDatabaseTestExecutionListener
}
// remove cleanAll, cleanReferenceData, unused cleaning code
}
*(optional) Add SQL initialization scripts (global and local)
// [...]
@Sql(scripts = "scripts/test-global-init.sql")
@SqlMergeMode(MergeMode.MERGE)
class AbstractApplicationTestCase extends AbstractTestCase {
// [...]
}
// [...]
class TestCase extends AbstractApplicationTestCase {
// [...]
@Sql(scripts = "scripts/test-testcase1.sql")
void testCase1() {
// [...]
}
}