Skip to main content

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

java
@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 filters
  • MockMvc for testing HTTP requests
  • No repository or service beans (unless mocked with @MockBean)

2. @DataJpaTest - Testing Repositories

java
@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

java
@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

java
@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

java
@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:

java
@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

java
@Entity
public class BlogPost {
@Id @GeneratedValue
private Long id;
private String title;
private String content;
private String author;
// getters and setters
}

Repository Layer

java
public interface BlogRepository extends JpaRepository<BlogPost, Long> {
List<BlogPost> findByAuthor(String author);
}

Repository Test

java
@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

java
@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

java
@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

java
@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

  1. Choose the right slice - Use the test slice that corresponds to the layer you're testing.

  2. Mock dependencies - Use @MockBean to mock dependencies outside your test slice.

  3. Keep tests focused - Test only what belongs to that layer; don't mix controller and service testing.

  4. Use multiple slice types - Create separate test classes for different layers.

  5. Consider test execution time - Use the lightest possible slice for your testing needs.

  6. Include custom configurations - You can use @Import to include specific configurations:

java
@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:

java
@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:

java
// 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

Exercises

  1. Create a simple Spring Boot application with a controller, service, and repository, then write tests for each layer using appropriate test slices.

  2. Extend the blog application example above with comment functionality and write tests for the new features.

  3. Create a custom test slice for a specific use case in your application.

  4. 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! :)