When working on tests integrating with the Spring Boot framework, it's good to know at least the basics of how the (testing) framework operates. One of these basics is that every Spring Boot application works with an application context. This context defines "everything" – beans, properties, dependencies and so forth – required to run an instance of the application. The test context is the application context required to run a specific test.

For example, if you would run the following two tests:

@SpringBootTest 
class AnIntegrationTest {
    @MockBean
    SomeService someService;

    // [...]
}

@SpringBootTest 
class AnotherIntegrationTest {
    @MockBean
    OtherService otherService;

    // [...]
}

There are two test contexts involved. By default, @SpringBootTest sets up an entire Spring Boot application context your test can run against. For these two tests, the application context cannot be reused. Why? Because one test wants to inject a mock of SomeService in the application context and the other test a mock of OtherService. The result is that the test contexts are different.

This slows things down

Depending on how big your application is, this could add up to considerable extra time in your test suite. Though the Spring Boot testing framework does cache test contexts for reuse, very little is needed to change the cache key of a test context. Injecting a different configuration property will change the test context and as seen in the example above, simply mocking a different bean will also change the cache key. The entire list making up the cache key can be found in the documentation.

It's good to know that @SpringBootTest comes with the most expensive default configuration which loads the entire application, which can be time consuming. The Spring Boot testing framework has various auto configuration classes defined which only initializes a specific 'slice' of the framework and your application classes. Even though each test slice might require a new application context, they start up faster because they are much narrower.

The following example loads two different application contexts, but the performance penalty is minimal as @WebMvcTest loads only the web-mvc-stack and is instructed to load only one specific controller class into the application context. The application contexts are very light-weight and start up considerably faster than a @SpringBootTest:

@WebMvcTest(controllers = SomeController.class)
class SomeControllerWebMvcTest {
    // [...]
}

@WebMvcTest(controllers = AnotherController.class)
class AnotherControllerWebMvcTest {
    // [...]
}

Finding the culprit(s)

To quickly find out if you could improve something regarding the amount of test contexts being created, is getting in some extra logging. Enable the test context cache logging:

logging.level.org.springframework.test.context.cache=DEBUG

Which will produce these kind of log lines:

Storing ApplicationContext [...] in cache under key
    [[WebMergedContextConfiguration [...]]
ApplicationContext cache statistics: [size = 16, 
    maxSize = 32, hitCount = 412, missCount = 16]

If you're seeing a lot of cache misses, it might indicate there is something to optimize by defining test contexts which can be reused more often. For example, by introducing a test profile or generalizing configuration which can act as a common denominator for more tests. To get a clearer overview of how much time is spent on initializing new contexts, it can be useful to also define a (temporary) global ApplicationContextInitializer for the META-INF/spring.factories of your test resources:

# src/test/resources/META-INF/spring.factories
org.springframework.context.ApplicationContextInitializer=com.company.InitializerTimer

And the actual class would look something like this:

public class InitializerTimer
    implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    long startTimeMs;

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        startTimeMs = System.currentTimeMillis();
        applicationContext.addApplicationListener(e -> {
            if (e instanceof org.springframework.context.event.ContextRefreshedEvent) {
                long endTimeMs = System.currentTimeMillis();
                long durationMs = endTimeMs - startTimeMs;
                log.debug("Context refreshed in {} ms", durationMs);
            }
        });
    }
}

By then turning off all other logging (logging.level.root=OFF) you will get a concise overview of how the cache is being treated and how much time is spent loading new application contexts for your Spring tests. If there are contexts which take a long amount of time to initialize, you could consider whether it's possible to merge test contexts whilst keeping in mind the definition of the test context cache key to reduce cache misses.

Want more content like this?

Hi! I sporadically write about tech & business. If you want an email when something new comes out, feel free to subscribe. It's free and I hate spam as much as you do.

your@email.com Subscribe

Conclusion

As always with improving performance, it depends on the circumstances. Simply having a lot of test contexts and cache misses doesn't necessarily mean something is wrong with your test suite. Some test contexts start up faster than others and sometimes merging test contexts is not feasible. If you have Spring Boot integration tests, there is often at least one test context which takes a longer amount of time because the entire application has to start up.

It's however good to be aware of how the test contexts work in practice and sometimes it's worth taking a closer look to see if the performance can be improved to potentially speed up build times.

Quick tip: faster Spring Boot tests with better test contexts