Spring Test Slices
Introduction
When testing Spring applications, loading the entire application context for each test can be time-consuming and unnecessary. Spring Boot provides a solution through Test Slices - specialized annotations that load only the relevant parts of your application for testing specific layers or components. This approach creates faster, more focused tests and helps isolate problems more effectively.
Test slices are an essential tool in the Spring testing toolkit, allowing you to test different parts of your application without the overhead of a full Spring context. This is especially valuable in large applications where startup times can significantly impact your development workflow.
Understanding Test Slices
Spring Test Slices are specialized annotations that configure the Spring application context to include only the components relevant to testing a specific layer of your application. Each test slice:
- Loads a subset of your Spring beans
- Configures appropriate auto-configurations
- Provides specialized testing utilities
- Runs faster than a full context test
Common Test Slice Annotations
Let's explore the most commonly used test slice annotations in Spring Boot:
1. @WebMvcTest
- Testing Controllers
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testGetUser() throws Exception {
// Arrange
when(userService.getUser(1L)).thenReturn(new User(1L, "John", "Doe"));
// Act & Assert
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.firstName").value("John"));
}
}
This test loads only the web layer components:
- The specified controller
@ControllerAdvice
and filtersMockMvc
for testing HTTP requests- No repository or service beans (unless mocked with
@MockBean
)
2. @DataJpaTest
- Testing Repositories
@DataJpaTest
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
public void testFindByEmail() {
// Arrange
User user = new User();
user.setEmail("[email protected]");
user.setName("Test User");
userRepository.save(user);
// Act
User found = userRepository.findByEmail("[email protected]");
// Assert
assertNotNull(found);
assertEquals("Test User", found.getName());
}
}
@DataJpaTest
provides:
- Auto-configuration for JPA repositories
- In-memory H2 database by default
- Automatic transaction management
- Only repository beans are loaded
- Special utilities like
TestEntityManager
3. @DataMongoTest
- Testing MongoDB Repositories
@DataMongoTest
public class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Test
public void testFindByCategory() {
// Arrange
Product product = new Product();
product.setName("Test Product");
product.setCategory("Electronics");
productRepository.save(product);
// Act
List<Product> products = productRepository.findByCategory("Electronics");
// Assert
assertEquals(1, products.size());
assertEquals("Test Product", products.get(0).getName());
}
}
4. @RestClientTest
- Testing REST Clients
@RestClientTest(WeatherService.class)
public class WeatherServiceTest {
@Autowired
private WeatherService weatherService;
@Autowired
private MockRestServiceServer server;
@Test
public void testGetWeatherForecast() {
// Arrange
String responseJson = "{\"temperature\": 25, \"condition\": \"Sunny\"}";
server.expect(requestTo("/api/weather/New York"))
.andRespond(withSuccess(responseJson, MediaType.APPLICATION_JSON));
// Act
WeatherForecast forecast = weatherService.getForecast("New York");
// Assert
assertEquals(25, forecast.getTemperature());
assertEquals("Sunny", forecast.getCondition());
server.verify();
}
}
This test slice focuses on REST clients:
- Auto-configures
RestTemplateBuilder
- Provides a
MockRestServiceServer
for testing without real HTTP calls - Only loads specified beans
5. @JsonTest
- Testing JSON Serialization/Deserialization
@JsonTest
public class UserJsonTest {
@Autowired
private JacksonTester<User> json;
@Test
public void testSerialize() throws Exception {
User user = new User(1L, "John", "Doe");
// Act
JsonContent<User> result = json.write(user);
// Assert
result.assertThat()
.hasJsonPathNumberValue("$.id", 1)
.hasJsonPathStringValue("$.firstName", "John")
.hasJsonPathStringValue("$.lastName", "Doe");
}
@Test
public void testDeserialize() throws Exception {
String content = "{\"id\": 1, \"firstName\": \"John\", \"lastName\": \"Doe\"}";
// Act
User user = json.parse(content).getObject();
// Assert
assertEquals(1L, user.getId());
assertEquals("John", user.getFirstName());
assertEquals("Doe", user.getLastName());
}
}
@JsonTest
focuses on JSON testing with utilities for:
- Jackson, Gson, or JSON-B
- AssertJ-based assertions for JSON content
Creating a Custom Test Slice
You can create custom test slices for specific testing scenarios in your application:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(SecurityTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureKafka
@ImportAutoConfiguration
public @interface KafkaServiceTest {
// Define custom properties
}
Building Layered Tests with Test Slices
Let's build a complete example using test slices for a simple blog application:
Domain Model
@Entity
public class BlogPost {
@Id @GeneratedValue
private Long id;
private String title;
private String content;
private String author;
// getters and setters
}
Repository Layer
public interface BlogRepository extends JpaRepository<BlogPost, Long> {
List<BlogPost> findByAuthor(String author);
}
Repository Test
@DataJpaTest
public class BlogRepositoryTest {
@Autowired
private BlogRepository blogRepository;
@Test
public void testFindByAuthor() {
// Arrange
BlogPost post1 = new BlogPost();
post1.setTitle("First Post");
post1.setContent("Content 1");
post1.setAuthor("john");
BlogPost post2 = new BlogPost();
post2.setTitle("Second Post");
post2.setContent("Content 2");
post2.setAuthor("jane");
blogRepository.saveAll(Arrays.asList(post1, post2));
// Act
List<BlogPost> johnsPosts = blogRepository.findByAuthor("john");
// Assert
assertEquals(1, johnsPosts.size());
assertEquals("First Post", johnsPosts.get(0).getTitle());
}
}
Service Layer
@Service
public class BlogService {
private final BlogRepository blogRepository;
public BlogService(BlogRepository blogRepository) {
this.blogRepository = blogRepository;
}
public List<BlogPost> getPostsByAuthor(String author) {
return blogRepository.findByAuthor(author);
}
public BlogPost createPost(BlogPost post) {
return blogRepository.save(post);
}
}
Controller Layer
@RestController
@RequestMapping("/api/blogs")
public class BlogController {
private final BlogService blogService;
public BlogController(BlogService blogService) {
this.blogService = blogService;
}
@GetMapping("/author/{author}")
public List<BlogPost> getPostsByAuthor(@PathVariable String author) {
return blogService.getPostsByAuthor(author);
}
@PostMapping
public BlogPost createPost(@RequestBody BlogPost post) {
return blogService.createPost(post);
}
}
Controller Test
@WebMvcTest(BlogController.class)
public class BlogControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BlogService blogService;
@Test
public void testGetPostsByAuthor() throws Exception {
// Arrange
BlogPost post = new BlogPost();
post.setId(1L);
post.setTitle("Test Post");
post.setContent("Test Content");
post.setAuthor("john");
when(blogService.getPostsByAuthor("john"))
.thenReturn(Collections.singletonList(post));
// Act & Assert
mockMvc.perform(get("/api/blogs/author/john"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Test Post"))
.andExpect(jsonPath("$[0].content").value("Test Content"));
}
@Test
public void testCreatePost() throws Exception {
// Arrange
BlogPost post = new BlogPost();
post.setTitle("New Post");
post.setContent("New Content");
post.setAuthor("john");
BlogPost saved = new BlogPost();
saved.setId(1L);
saved.setTitle("New Post");
saved.setContent("New Content");
saved.setAuthor("john");
when(blogService.createPost(any(BlogPost.class))).thenReturn(saved);
// Act & Assert
mockMvc.perform(post("/api/blogs")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"New Post\",\"content\":\"New Content\",\"author\":\"john\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("New Post"));
}
}
Best Practices for Using Test Slices
-
Choose the right slice - Use the test slice that corresponds to the layer you're testing.
-
Mock dependencies - Use
@MockBean
to mock dependencies outside your test slice. -
Keep tests focused - Test only what belongs to that layer; don't mix controller and service testing.
-
Use multiple slice types - Create separate test classes for different layers.
-
Consider test execution time - Use the lightest possible slice for your testing needs.
-
Include custom configurations - You can use
@Import
to include specific configurations:
@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
public class UserControllerWithSecurityTest {
// Test methods
}
Common Issues and Solutions
1. Missing Required Beans
Problem: NoSuchBeanDefinitionException
when a required bean isn't in the test slice.
Solution: Use @MockBean
or @Import
to provide the missing bean:
@WebMvcTest(OrderController.class)
public class OrderControllerTest {
@MockBean
private OrderService orderService;
// Test methods
}
2. Slow Test Execution
Problem: Tests using @SpringBootTest
are slow to start.
Solution: Replace with appropriate test slices when possible:
// Slower
@SpringBootTest
public class SlowTest {
// Test methods
}
// Faster
@WebMvcTest(UserController.class)
public class FastTest {
// Test methods
}
3. Complex Dependencies
Problem: Components with many dependencies are hard to test with slices.
Solution: Consider refactoring for better separation of concerns, or use component testing with explicit @Import
statements.
Summary
Spring Test Slices provide a powerful way to create focused, fast tests for different layers of your Spring application. By loading only the components relevant to what you're testing, you can:
- Speed up test execution
- Improve test isolation
- Focus on specific layers
- Identify issues more precisely
The main test slices we've covered include:
@WebMvcTest
for controllers@DataJpaTest
for JPA repositories@DataMongoTest
for MongoDB repositories@RestClientTest
for REST clients@JsonTest
for JSON serialization/deserialization
By choosing the right test slices for each layer of your application, you can build a comprehensive test suite that runs quickly and effectively identifies issues.
Additional Resources
- Spring Boot Test Slice Documentation
- Testing Web Applications with Spring Boot
- Testing Spring Data JPA Applications
Exercises
-
Create a simple Spring Boot application with a controller, service, and repository, then write tests for each layer using appropriate test slices.
-
Extend the blog application example above with comment functionality and write tests for the new features.
-
Create a custom test slice for a specific use case in your application.
-
Compare the execution time of a test using
@SpringBootTest
versus the same test using a test slice.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)