Speed up your Spring tests with Test Slices
Paul Royer5 min read
Prevent reboots from slowing down your tests
Navigating through the complex labyrinth of code testing is an inevitable adventure for developers. Many teams face a situation where they work a long time before hearing of key skills that would help them on this journey. Here is the story of one of these skills, how I discovered it, and how you can quickly master it.
As I went through the test logs of my Spring Boot application, I quickly caught a glimpse of the Spring Banner, indicating the app started to test a controller. A few seconds later, I did see a second Spring banner: the app restarted to run the tests of another controller. And a third banner/reboot for a third controller. That quickly turns into dozens. With about 10 seconds to start the app (identifying and connecting beans, executing database migrations…), minutes of testing were spent in reboots.
Test performance dropped. But this is no fatality: let’s dive into Spring Test Slices to run faster tests through faster reboots.
Controllers’ testing needs the application to start
When testing your controller you want to check that:
- the path of the route is correctly set,
- the authorization layer is called with the correct arguments,
- path variable, query parameters and body deserialization and validation are properly done,
- return code conforms to expectations,
- services are called with the correct parameters.
Apart from the last check, unit tests cannot be used. You need to start the application for these checks; this is often done with the @SpringBootTest
annotation.
Why Spring reboots the application between controllers’ tests?
So you’re using integration test, and you end up starting your whole application. It’s a bit long but once the application has started, why don’t all your controllers’ tests run in a row? Why all these reboots?
This behavior can be explained by diving into how the ApplicationContext
interface works. Simply, the Application Context is responsible for assembling and managing your app beans (controllers, services, repositories…). If the Application Context change, the app will reboot. And a new Application Context is created if the referenced beans are different (or if application properties differ). But this happens when using a mock (through the @MockBean
annotation): the normal bean will be replaced in the Application Context by the mock bean.
It’s the key to our problem: in every controller test class, some specific services have to be mocked, causing the Application Context to change, and the app to reboot. The app will reboot from one test class to another if the mocked services differ.
Optimize the scope of your test with slices
A great part of the reboot time is due to starting the whole application, including things we don’t truly need to test our controllers:
- Services, which are mocked
- Repositories - which are not called by controllers - plus the database and applying migrations
- And even other controllers than the one we’re currently testing
To test our controller, we only need to start the app with the beans required in the controller scope (such as @Controller
, @ControllerAdvice
, @JsonComponent
…). This can be done using a test slice: it allows you to start the application with a customized ApplicationContext
for your test.
For example, you can use the @WebMvcTest
annotation to start a test slice centered around your controller:
@WebMvcTest(controllers = { MyController.class, ApplicationProperties.class })
class MyControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
MyService myService;
@Test
void testSuccess() {
//...
}
Using test slice, your app will start with a very light ApplicationContext
, thus starting way faster! You will reduce the feedback loop of your test while running them in your IDE or your CI. On the code base we tried it out on switching from regular integration tests to test slices, the time to run a single controller test class from the IDE fell from approx. 15 seconds each to 5 seconds. CI duration reduced from 20%, saving 50s each time triggered!
Slice test your app!
Test slices are performant, could we use them for other beans than controllers?
Services? As we test services through unit tests that do not require starting the application, test slices are of no use in this case.
Repositories? Yes! Why start the whole application if you just want to test the interactions between your app and your database? The @DataJpaTest
annotation will start only the relevant beans to test your repositories.
@DataJpaTest(showSql = false)
public class MyRepositoryTest {
@Autowired
TestEntityManager entityManager;
@Autowired
MyRepository myRepository;
@Test
void testSuccess() {
//...
}
@DataJpaTest
has also some useful features such as making tests transactional by default, and rolling back at the end of each test. They also use an embedded in-memory database (you still can configure these settings). And such as @WebMvcTest
, it will speed up your tests!
Conclusion
Testing your Spring Boot app using mocks implies a great number of reboots, which can slow your tests down to a crawl. Rebooting only the part of your app linked to the bean you’re testing using test slices will accelerate your test suites, provide you with quicker TDD feedback and save up to minutes of your CI!