Have you ever wondered what’s happening behind the scenes when you run tests using spring boot? Usually, you don’t care until it’s working in a predictable way. Once you encounter performance issues, your PC starts to lag, a single test executes in a couple of seconds instead of milliseconds, then you start digging. Most of the time, you end up with a single culprit — the spring context caching mechanism. It’s a great “tool” to speed up your tests, but you must be aware of all the consequences it entails.

Investigation

Let’s take a step back and start with a problem we had in the project. We have a spring boot application with mongo as storage. We have more than five hundred tests which start up a spring context and use embedded mongo beneath. A couple of weeks ago, we decided to bump up the version of embedded mongo to have it aligned with the one we have in real. A small change in gradle.properties and application.properties, rerun all test suites and … my computer stopped responding. Upsss… I needed a hard reset to bring it back to life ;-)

We reverted the change but a few days later, we decided to investigate the problem. The first thing we noticed was that if you run a single test, everything goes smoothly; the problem occurs only when running the whole suite (and only on some computers). Ok, so what’s the difference between running a single test or a single test class, versus a whole suite?

We played with embedded mongo settings, so our obvious culprit was checking it while tests were in progress. Here’s a quick look at it with simple ps commands while tests are executed:

The number of instances of embedded mongo was growing, and it reached 7 by the end of tests. Magic number? ;-) Of course not — we realized at a glance that this number is closely correlated with the number of spring contexts we started up in tests.

Spring context cache mechanism

This is what we expected despite one fact. When the spring context starts and is never used again, the embedded mongo should then be shut down, right? But wait… how can spring determine that you won’t use this spring context anymore? That’s the point! It caches each context for optimization reasons, and shuts them all down right after the last test. It does it because the spring context may be reused, and spring doesn’t know upfront until it reaches the same context once more. It means that during the suite execution, each start of a new context increases the number of active embedded mongo instances!

For some reason, such numbers of concurrent active mongo instances weren’t a problem with embedded mongo v3.4 (used before), but for v4.2 it became an issue. CPU utilization on my machine was reaching 100%, and test suite execution was unable to reach the end.

Reducing spring contexts

The first thought in our mind was “let’s reduce the number of spring contexts”. Spring starts a new context for each new configuration in the test. You can look at class ContextCache and get the method. It returns ApplicationContext for a key which is a type of MergedContextConfiguration. A quick look at its javadoc “MergedContextConfiguration encapsulates the merged context. (…) Merged context resource locations, annotated classes, active profiles, property resource locations, and in-lined properties represent all declared values (…)”. It means that each small change in configuration (even one line in properties) creates a new spring config.

In our project, it turned out that each of the 7 different spring test configurations is justified, so we cannot reduce spring contexts.

One mongo instance to the rescue

Ok, we cannot win with a spring context number, but do we need 7 different instances of mongo, started on different ports with a separated data set? Well … no! Each of our tests has a tearDown method which clears all mongo collections. This allows us to share one mongo instance between all started spring contexts. Great news, but how do we do that? Where do we start?

As you might expect, there should be some Configuration class responsible for setting up embedded mongo, and you’re right — there is. EnableAutoConfiguration class in given conditions enables class EmbeddedMongoAutoConfiguration. This configuration produces 2 beans — embedded mongo server and configuration for it. If we can somehow override the creation of it and keep one instance of bean, we should be out of the woods. Ok, let’s try.

Disclaimer: If you’re using @EnableAutoConfiguration then remember to exclude the default EmbeddedMongoAutoConfiguration not to conflict with the newly created SingleEmbeddedMongoConfiguration. Just type @EnableAutoConfiguration(exclude = EmbeddedMongoAutoConfiguration.class) and it’s done.

When it comes to sharing one instance of a class within a single class loader, the singleton pattern comes to mind. So let’s implement the creation of the embedded mongo server this way:

The classic approach to the singleton pattern — factory method embeddedMongoServer uses static field (1), to return the value assigned to it when it’s present (3) or create a new one (4) when it’s called the first time.

Thanks to this simple trick, no matter how many spring contexts start during your tests, they’ll share a single instance of MongodExecutable. As you may see, we return (4) some special kind of MongoExecutable which is called SingleInstanceMongodStarter. What’s more, we instruct spring (2) that each time the spring context is requested to produce embeddedMongoServer bean, it should call the start method as the initial one, and stop when, due to a shutdown of context, a bean is about to be destroyed. Let’s see what we need these 2 methods for:

Counting mongo start references

When spring calls the start method on bean, we perform the same “trick” as in configuration, but we count how many times we were called as well. If the counter is zero (first call) then we initiate a process (2) responsible for starting the embedded mongo process and increment the counter. If it’s not the first call (1), we simply increment the counter and return the already created process.

The related logic goes to the stop method. If the decremented counter is zero, it means we are shutting down the last spring context. In this way, we can stop the embedded mongo process.

As I mentioned earlier, this logic can be applied thanks to caching the spring contexts mechanism. All started contexts will be shut down after the last test, causing the stop method to be called many times. The last one will shut down the mongo.

I omitted some sections of code to achieve more readable code snippets. The whole implementation is here.

Conclusions

If you have a lot of tests with spring under the hood, remember that:

spring starts a new context for each configuration, even when there is a small difference between them

spring caches the context for each configuration until the end of a test suite

when you start a given bean in the configuration, you’ll have as many beans as configurations — this fact can have a significant impact on resource utilization

starting spring context takes time and consumes resources, so be reasonable when deciding to write a test with “dedicated” spring configurations

you can (or maybe even should) share one bean instance between many contexts if this bean consumes many resources

If your tests are slow and/or resource-consuming, I trust I’ve encouraged you to investigate further. Hopefully you’ll find my advice useful. Happy testing!