Search code examples
javaspring-bootintegration-testingspring-boot-test

Spring Boot tests - assert max number of started Spring application contexts


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?


Solution

  • 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.