Magnus' blog


Faster Spring Boot Test Suite

In Hibernate TestBase I showed how to build a single EntityManagerFactory shared by all tests. That works well when the tests don’t use any Spring infrastructure, which is the strategy I prefer. However, I was recently asked about a standard Spring Boot setup. Could the strategy for running all tests in a single database shown in Integration Testing with a Real Database be combined with Speeding Up Multi-module Gradle Integration Testing to speed it up?

It turns out that it is quite easy to do in Spring. First, all tests should either inherit from a base class with the following annotations or have the annotations applied directly:

@SpringBootTest
@ActiveProfiles("test")
@Transactional
public abstract class BaseIntegrationTest { }

@Transactional works differently in a test, where it will roll back the transaction at the end. Therefore, this works the same way as the Integration Testing with a Real Database approach. The same caveats apply too. Anything that depends on a commit, such as a @TransactionalEventListener on the AFTER_COMMIT phase, will never run because nothing commits. REQUIRES_NEW propagation is the opposite problem: it suspends the test transaction and commits its own, so that data survives the rollback and leaks into the shared database. Tests that genuinely need to commit can opt out with @Commit, but they must then follow the rules for committed data described in that post.

The database is configured by placing an application-test.properties file on the test classpath with the connection values:

spring.datasource.url=jdbc:postgresql://localhost:5432/app_test
spring.datasource.username=app_test
spring.datasource.password=app_test

The @ActiveProfiles("test") annotation specifies that application-test.properties should be used, since the profile name matches the file suffix.

The last thing to do is to create a JUnit Platform test suite as presented in Speeding Up Multi-module Gradle Integration Testing:

@Suite
@SuiteDisplayName("All module integration tests")
@SelectPackages("dev.mhh")
class IntegrationTestSuite { }

The suite class lives in a dedicated test-suite module that depends on every other module’s test output, exactly as in the earlier post. That puts all the test classes on one classpath, so the suite can select them across modules. Running the test-suite module then runs every integration test in a single JVM.

Now the tests can be run either individually or as a suite. In the suite, they will all share the same ApplicationContext and therefore the same EntityManagerFactory, making them much faster. This works because Spring caches the ApplicationContext keyed by the test configuration. As long as every test class resolves to the same key, the context is only built once per JVM.

That is also why annotations like @MockitoBean, @ActiveProfiles, @Import or @TestPropertySource that differ from the base class will cause a new context to be created, since they change the key. @DirtiesContext will instead evict the shared context from the cache. Either way the EntityManagerFactory is recreated, so they should not be used in this setup.

The example also assumes that the whole project is auto configured. @SpringBootTest locates the @SpringBootApplication class and lets component scanning and entity scanning pick up everything below it, so the base class needs no further configuration. Projects that instead place @EntityScan and @Import on individual test classes get a different key per test. Those annotations have to be migrated to the auto configured setup, either by moving them to the base class or by letting the application class cover them, so that every test resolves to the same context. Alternatively, there can be one auto configuration for the individual tests and another for the test suite. That gives a slight variation between running a single test and running the suite, but it might be easier to set up.

The attributes of @SpringBootTest are part of the key too, so properties, classes or webEnvironment set on a test class also create a new context. webEnvironment = RANDOM_PORT additionally handles requests on a server thread, outside the test transaction, so controller writes are committed and never rolled back. Web layer tests should use MockMvc instead, which runs the request on the test thread, inside the transaction.

Most Spring Boot projects already have @SpringBootTest on their integration tests, so the actual changes are @Transactional, a shared test profile and the suite module. For that small amount of work, a project on the default Spring Boot test setup gets a speedup similar to the hand-built Hibernate setup.

If you can follow the rules through, you can benefit from a similar speedup. The rules are not hard to state: one context key for every test, no annotations that change it, no @DirtiesContext, no commits, and web tests on MockMvc. They are hard to keep. Every one of them is a single annotation away from being broken, and Spring will not complain when it happens. It silently builds a second context and the suite gets slower, often without anyone noticing until the next time someone looks at the build times. Spring’s test support is designed to make a @MockitoBean or a @TestPropertySource convenient to add, and so they get added. In my opinion it is incredibly hard to maintain this in a Spring setup, which is why I still prefer the manual setup presented in the earlier posts. There the shared EntityManagerFactory is a plain object that the tests cannot accidentally replace.

View next or previous post: