I have multiple integration tests with @SpringBootTest
.
In some cases, Spring Boot starts multiple application contexts by executing these integration tests (e.g. due to usage of @MockBean
and @SpyBean
on specific test class instead of defining them on common abstract class), and it leads to increased build time or even worse - might crash surefire plugin execution (like in my case). The following example with two test classes starts two app contexts:
@SpringBootTest
class AbstractIntegrationTest {
..
}
class IntegrationTest1 extends AbstractIntegrationTest {
@MockBean
private Service1 service1;
@Test
void someTest1() { ... }
}
class IntegrationTest2 extends AbstractIntegrationTest {
@MockBean
private Service1 service1;
@Test
void someTest2() { ... }
}
but if we move the same field with @MockBean
into abstract class, only a single app context will be started.
After identifying and fixing all those cases, now I have only two started application contexts (the first one is without any mocked beans and the second is with mock and spy beans). I found that there is DefaultContextCache
that handles created application contexts, but unclear how to reach that. We could turn on logging by this class (logging.level.org.springframework.test.context.cache: debug
) and it will log the following messages:
Spring test ApplicationContext cache statistics: [DefaultContextCache@44351c52 size = 8, maxSize = 32, parentContextCount = 0, hitCount = 2156, missCount = 8]
by this example size = 8
- displays number of application contexts that are cached (and in this case it's the total number of app contexts, and there were no evictions by reaching max size). This is useful log for described scenario, but I want to have test that make expected assertion, to prevent having new extra application contexts by mistake in the future.
I would like to have assertion test that verifies the total number of started application contexts after executing all tests. How to achieve that?
We could define custom TestExecutionListener
, register it as part of TestExecutionListeners
and declare test with @AfterAll
from JUnit5.
TestExecutionListener
has methods beforeTestClass
and afterTestClass
that accept TestContext
, and it has reference to ApplicationContext
.
public class ApplicationContextAwareTestExecutionListener extends AbstractTestExecutionListener {
private static final Set<ApplicationContext> applicationContexts = new HashSet<>();
@Override
public void beforeTestClass(TestContext testContext) {
ApplicationContext applicationContext = testContext.getApplicationContext();
applicationContexts.add(applicationContext);
}
public static int getApplicationContextsCounter() {
return applicationContexts.size();
}
}
and adding test assertTotalNumberOfCreatedApplicationContexts
to assert that :
@SpringBootTest
@TestExecutionListeners(
listeners = ApplicationContextAwareTestExecutionListener.class,
mergeMode = MergeMode.MERGE_WITH_DEFAULTS
)
public abstract class IntegrationTest {
private static final int MAX_ALLOWED_TEST_APPLICATION_CONTEXTS = 2;
@AfterAll
public static void assertTotalNumberOfCreatedApplicationContexts() {
int applicationContextsCounter = ApplicationContextAwareTestExecutionListener.getApplicationContextsCounter();
log.info("total number of created application contexts: {}", applicationContextsCounter);
assertThat(applicationContextsCounter)
.isLessThanOrEqualTo(MAX_ALLOWED_TEST_APPLICATION_CONTEXTS);
}
}
Another option is to get desired object DefaultContextCache
from object TestContext
by reflection, but I guess it's less attractive and more fragile (might be broken with newer versions of Spring-Test, as here we depend on private fields). Here TestContext
is instantiated as DefaultTestContext
that has field cacheAwareContextLoaderDelegate
and instantiated as DefaultCacheAwareContextLoaderDelegate
. It has method getContextCache()
that returns DefaultContextCache
.