Improve build times with Context Caching from Spring Test

Last Updated:  August 27, 2021 | Published: July 29, 2020

I've recently invested one day to improve the integration test setup for a larger project. The result is amazing: the build time (running mvn clean verify) went down from 25 minutes to 5 minutes by solely focussing on the test setup. All of this was possible by making the most of the Spring Test Context Caching mechanism. Continue reading to get an introduction to this and use my recommendations to also reduce your build times. Nothing is more tiresome than long feedback cycles due to slow builds.

Spring Test Context Caching Mechanism

Whenever your tests go beyond trivial unit testing and you include Spring Test features, you'll most likely start a customized Spring Context (e.g. @SpringBootTest, @WebMvcTest, @DataJpaTest). There is a lot of test support available that allows you to create an application context that fits your testing purpose (e.g. include only web-layer related beans when using @WebMvcTest).

Starting a Spring context for your test takes a good amount of time. Especially if you populate the whole context using @SpringBootTest. This adds an additional overhead (time-wise) to your test execution and may drastically increase your total build time.

Fortunately, Spring Test provides a mechanism to cache an already started application context and reuse it for subsequent tests. You can think of a basic Map that stores different application contexts. Spring Test creates a uniquely identifiable key for the Map entry.

This is important as not every application context can be used for all tests. Using a web context for a @DataJpaTest won't work and also a context that uses an application profile of integration-test might not work for a test using the default profile.

That's why Spring Test creates this unique key for a context from a set of properties/configuration parameters that define it:

  • locations (from @ContextConfiguration)
  • classes (part of @ContextConfiguration)
  • contextInitializerClasses (from @ContextConfiguration)
  • contextCustomizers (from ContextCustomizerFactory) – e.g. @DynamicPropertySource, @MockBean and @SpyBean.
  • contextLoader (part of @ContextConfiguration)
  • parent (from @ContextHierarchy)
  • activeProfiles (coming from @ActiveProfiles)
  • propertySourceLocations (from @TestPropertySource)
  • propertySourceProperties (from @TestPropertySource)
  • resourceBasePath (part of @WebAppConfiguration)

Whenever one of these configuration parameters changes, the context key will also change. If there is no entry in the Map, Spring Test starts a new context for you. On the other side, if most of your tests share the same context configuration, you can reuse the Spring context for your test and reduce your build times.

That's the theory. Let's take a look at what usually goes wrong or prevents this neat feature of Spring Test to shine.

Pitfalls When Trying to Reuse a Context

Most of the time the usage of either @MockBean or @SpyBean prevents you from reusing an already started application context. What I've seen a lot is using these annotations to short-circuit the testing functionality of your application.

Consider your application subscribes to a JMS queue and you want to verify the processing. During your integration test you put a message into your queue (e.g. while using Testcontainers) and start the whole Spring context (@SpringBootTest). The lazy developer might simply add mocked beans for the processing logic and will just verify that the message was processed by a consumer.

Some developers might argue that testing everything together is hard and they simply don't know how to do it. While this is a valid argument, consider not to start the whole Spring context or ask yourself if you are actually testing library functionality.

A different place for this short-circuiting is sometimes found at the web-layer:

Another pitfall I often see is the excessive use of @DirtiesContext. There might be good reasons to use this annotation, e.g. when you modify the global application state in a test. But most of the time, it's rather laziness of the developers when they encounter an issue while running all tests together. Usually, this occurs when testing messaging or database-related parts.

Rather than taking the time to understand the root cause of the problem, you can easily help yourself with @DirtiesContext. Using this annotation you mark the Spring context as dirty and Spring Test won't reuse it. Once someone introduces this annotation, the chances are high that the next developer will pick it up and nobody will take time to fix it as the window is already broken.

Furthermore, using multiple different profiles for your tests with @ActiveProfiles is also a common pitfall. Try to avoid having multiple test profiles (e.g. it, it-web-test, it-database) and limit it to the smallest amount possible.

Let's take a look at further techniques to make the most of the context reusability feature of Spring Test.

Techniques for Efficient Context Reusing

In general, you should avoid modifying any global state inside your application context that prevents reusing it. Make sure to always clean up resources and leave the Spring Context in a clean state after each test execution.

Try to stick as much as possible to the same context configuration for your integration tests. To achieve this, you can either introduce an abstract parent class, that includes your common configuration:

… or write your own annotation that includes all configuration annotations, e.g. @FullIntegrationTest.

Whenever you are tempted to short-circuit a test using @MockBean, rethink. Especially when using this in conjunction with @SpringBootTest. There is a good chance that Spring provides an annotation to started a sliced context for the part of your application you want to test. Furthermore, you can also prepare a custom context with only the beans you need to reduce the startup time of the context.

If you currently parallelize the execution of e.g. your integration tests using either JUnit 5 or the Failsafe plugin, Spring Test might not be able to reuse the contexts. If your tests don't always run within the same process, you won't benefit from the context caching mechanism. Try disabling the parallelization and see if the context caching outperforms the parallelization of tests.

Insights Into the Context Caching of Spring Test

If you are curious why your current test does not reuse an already context, you can enable debug logs. This will also show statistics about the context cache:

While the default cache size is 32, you can increase or decrease this value:

Whenever the internal Spring context cache limit is reached, Spring Test uses a least recently used (LRU) eviction policy to make space for the next context.

If you want to learn more about this feature of Spring Test in action and testing Spring Boot applications in general, take a look at my recently announced Testing Spring Boot Applications Masterclass.

You can find the demo application with examples on GitHub.

Happy context caching with Spring Test,

Phil

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

    Join Our Mailing List To Get 3x Free Cheat Sheets

    Free Java Cheat Sheets
    >