Testing Spring Boot applications efficiently requires understanding the testing infrastructure Spring provides.
Three commonly used components – SpringRunner
, SpringExtension
, and @SpringBootTest
– often cause confusion among developers. Many of us find ourselves stacking these annotations without fully grasping their distinct purposes and how they complement each other.
In this article, we’ll clarify the differences between these testing components, explore when to use each one, and demonstrate how they can work together. By the end, we’ll have a clear understanding of the Spring testing ecosystem and be able to write more effective tests for our Spring Boot applications.
The JUnit Evolution: SpringRunner vs. SpringExtension
The first point of confusion often stems from the relationship between SpringRunner
and SpringExtension
.
Let’s clarify this with some background and code examples.
SpringRunner – The JUnit 4 Approach
SpringRunner
is a JUnit 4 test runner that bridges Spring’s testing capabilities with JUnit 4’s runner framework.
It’s an alias for SpringJUnit4ClassRunner
and provides the core functionality needed to run tests that use Spring test features.
Here’s an example of a JUnit 4 test class using SpringRunner
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class UserServiceJUnit4Test { @Autowired private UserService userService; @Test public void testUserCreation() { User savedUser = userService.createUser(user); assertNotNull(savedUser.getId()); } } |
The @RunWith(SpringRunner.class)
annotation tells JUnit 4 to use Spring’s testing support.
It’s responsible for creating the application context, enabling dependency injection, and managing test lifecycle events.
SpringExtension – The JUnit 5 Approach
With JUnit 5 (Jupiter), the extension model replaced the runner concept from JUnit 4. SpringExtension
is Spring’s implementation of this new extension model, providing equivalent functionality to SpringRunner
but for JUnit 5.
Here’s the same test written for JUnit 5 using SpringExtension
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @ExtendWith(SpringExtension.class) @SpringBootTest class UserServiceJUnit5Test { @Autowired private UserService userService; @Test void testUserCreation() { User savedUser = userService.createUser(user); assertNotNull(savedUser.getId()); } } |
The key difference is that we use @ExtendWith(SpringExtension.class)
instead of @RunWith(SpringRunner.class)
. Both annotations fulfill the same purpose: integrating Spring’s testing infrastructure with the respective JUnit version.
Understanding @SpringBootTest
While SpringRunner
and SpringExtension
provide the integration between Spring and JUnit, @SpringBootTest
defines what and how to test. It configures the Spring application context for our tests, loading the complete application configuration.
The @SpringBootTest
annotation offers various properties to customize our test environment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {"spring.datasource.url=jdbc:h2:mem:testdb"} ) @ActiveProfiles("test") class ConfiguredApplicationTest { @Autowired private UserRepository userRepository; @Test void testUserRepository() { } } |
In this example:
- We specify
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
to start the embedded server with a random port - We override a property with
properties = {"spring.datasource.url=jdbc:h2:mem:testdb"}
- We activate the “test” profile with
@ActiveProfiles("test")
These configurations allow us to precisely control our test environment without changing our application code.
Combining the Components: Best Practices
Now that we understand each component individually, let’s explore how they work together and establish best practices for different testing scenarios.
JUnit 5 Simplified Configuration
In Spring Boot 2.2.0 and later with JUnit 5, the @SpringBootTest
annotation includes @ExtendWith(SpringExtension.class)
by default, allowing us to simplify our test code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringBootTest class SimplifiedUserServiceTest { @Autowired private UserService userService; @Test void testUserCreation() { User savedUser = userService.createUser(user); assertNotNull(savedUser.getId()); } } |
This simplified approach is recommended for JUnit 5 tests. However, if we’re using JUnit 4, we still need to explicitly include @RunWith(SpringRunner.class)
.
Choosing the Right Testing Scope
While @SpringBootTest
loads the entire application context, sometimes we want more focused tests. Spring provides additional testing annotations for different scopes:
Web Layer Testing with MockMvc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void testCreateUser() throws Exception { mockUser.setId(1L); when(userService.createUser(any(User.class))).thenReturn(mockUser); mockMvc.perform(post("/api/users") .contentType("application/json") .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value(1)) } } |
@WebMvcTest
focuses on testing the web layer only and implicitly includes SpringExtension
. It’s faster than @SpringBootTest
because it only loads the web-related components.
Data Layer Testing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@DataJpaTest class UserRepositoryTest { @Autowired private UserRepository userRepository; @Test void testFindByEmail() { userRepository.save(user); assertEquals("[email protected]", userRepository.findByEmail("[email protected]").get().getEmail()); } } |
@DataJpaTest
focuses on testing JPA components, configuring an in-memory database and including only JPA-related beans. Like @WebMvcTest
, it implicitly includes SpringExtension
.
Summary
Understanding the differences between SpringRunner
, SpringExtension
, and @SpringBootTest
helps us write more effective and efficient tests for our Spring Boot applications:
SpringRunner
is for JUnit 4 and integrates Spring’s testing capabilities with JUnit’s runner modelSpringExtension
is for JUnit 5 and provides equivalent functionality using Jupiter’s extension model@SpringBootTest
configures the application context for our tests and can be customized with various properties
With Spring Boot 2.2.0+ and JUnit 5, we can simplify our test classes by just using @SpringBootTest
without explicitly adding @ExtendWith(SpringExtension.class)
. For JUnit 4, we still need @RunWith(SpringRunner.class)
.
By choosing the right testing scope – whether it’s a full application test with @SpringBootTest
, a web layer test with @WebMvcTest
, or a data layer test with @DataJpaTest
– we can make our tests more focused, faster, and more maintainable.
Remember that the goal is to test our application effectively while keeping tests simple and readable. The Spring testing framework provides the tools we need to achieve this balance.
Joyful testing,
Philip