Spring Boot Test Slices in Java

Introduction

Spring Boot Test Slices are specialized testing annotations that load only specific parts of your application context, making tests faster and more focused. Instead of loading the entire application context, slices allow you to test specific layers (web, data, security) in isolation.

Maven Dependencies

<dependencies>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Testcontainers for integration tests -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- Database testing -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Data Layer Testing

@DataJpaTest - JPA Repository Testing

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void whenFindByEmail_thenReturnUser() {
// given
User user = User.builder()
.email("[email protected]")
.firstName("John")
.lastName("Doe")
.build();
entityManager.persist(user);
entityManager.flush();
// when
User found = userRepository.findByEmail("[email protected]");
// then
assertThat(found).isNotNull();
assertThat(found.getEmail()).isEqualTo("[email protected]");
}
@Test
void whenFindByNonExistentEmail_thenReturnNull() {
// when
User found = userRepository.findByEmail("[email protected]");
// then
assertThat(found).isNull();
}
@Test
void shouldPersistUserWithAuditFields() {
// given
User user = User.builder()
.email("[email protected]")
.firstName("Audit")
.lastName("User")
.build();
// when
User saved = userRepository.save(user);
// then
assertThat(saved.getId()).isNotNull();
assertThat(saved.getCreatedAt()).isNotNull();
assertThat(saved.getUpdatedAt()).isNotNull();
}
}
// Entity class
@Entity
@Table(name = "users")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
private String firstName;
private String lastName;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
// Repository interface
public interface UserRepository extends JpaRepository<User, Long> {
User findByEmail(String email);
List<User> findByLastName(String lastName);
Optional<User> findByIdAndEmail(Long id, String email);
}

@DataJpaTest with Testcontainers

@DataJpaTest
@TestPropertySource(properties = {
"spring.test.database.replace=NONE",
"spring.datasource.url=jdbc:tc:postgresql:15-alpine:///testdb"
})
class UserRepositoryWithTestcontainersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldWorkWithRealPostgreSQL() {
User user = User.builder()
.email("[email protected]")
.firstName("Postgres")
.lastName("User")
.build();
User saved = userRepository.save(user);
assertThat(saved.getId()).isNotNull();
assertThat(userRepository.count()).isEqualTo(1);
}
}

@JdbcTest - JDBC Testing

import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
import org.springframework.jdbc.core.JdbcTemplate;
@JdbcTest
class UserJdbcRepositoryTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void shouldCountUsers() {
// given
jdbcTemplate.execute("CREATE TABLE users (id BIGINT PRIMARY KEY, email VARCHAR(255))");
jdbcTemplate.update("INSERT INTO users (id, email) VALUES (?, ?)", 1L, "[email protected]");
// when
Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", Integer.class);
// then
assertThat(count).isEqualTo(1);
}
@Test
void shouldInsertUser() {
// given
jdbcTemplate.execute("CREATE TABLE users (id BIGINT PRIMARY KEY, email VARCHAR(255))");
// when
int affectedRows = jdbcTemplate.update(
"INSERT INTO users (id, email) VALUES (?, ?)", 
2L, "[email protected]"
);
// then
assertThat(affectedRows).isEqualTo(1);
String email = jdbcTemplate.queryForObject(
"SELECT email FROM users WHERE id = ?", 
String.class, 2L
);
assertThat(email).isEqualTo("[email protected]");
}
}

@DataMongoTest - MongoDB Testing

import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.data.mongodb.core.MongoTemplate;
@DataMongoTest
class UserDocumentRepositoryTest {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private UserDocumentRepository userRepository;
@Test
void shouldSaveAndFindUser() {
// given
UserDocument user = UserDocument.builder()
.username("mongouser")
.email("[email protected]")
.build();
// when
UserDocument saved = userRepository.save(user);
// then
assertThat(saved.getId()).isNotNull();
assertThat(userRepository.findByUsername("mongouser"))
.isPresent()
.get()
.extracting(UserDocument::getEmail)
.isEqualTo("[email protected]");
}
@Test
void shouldFindByEmailUsingMongoTemplate() {
// given
UserDocument user = UserDocument.builder()
.username("templateuser")
.email("[email protected]")
.build();
mongoTemplate.save(user);
// when
UserDocument found = mongoTemplate.findOne(
Query.query(Criteria.where("email").is("[email protected]")), 
UserDocument.class
);
// then
assertThat(found).isNotNull();
assertThat(found.getUsername()).isEqualTo("templateuser");
}
}
@Document(collection = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class UserDocument {
@Id
private String id;
private String username;
private String email;
private LocalDateTime createdAt;
}
public interface UserDocumentRepository extends MongoRepository<UserDocument, String> {
Optional<UserDocument> findByUsername(String username);
List<UserDocument> findByEmailContaining(String email);
}

Web Layer Testing

@WebMvcTest - Controller Testing

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
void shouldReturnUser() throws Exception {
// given
UserResponse userResponse = UserResponse.builder()
.id(1L)
.email("[email protected]")
.firstName("John")
.lastName("Doe")
.build();
when(userService.getUserById(1L)).thenReturn(userResponse);
// when & then
mockMvc.perform(get("/api/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.email").value("[email protected]"))
.andExpect(jsonPath("$.firstName").value("John"))
.andExpect(jsonPath("$.lastName").value("Doe"));
}
@Test
void shouldCreateUser() throws Exception {
// given
CreateUserRequest request = CreateUserRequest.builder()
.email("[email protected]")
.firstName("Jane")
.lastName("Smith")
.build();
UserResponse response = UserResponse.builder()
.id(1L)
.email("[email protected]")
.firstName("Jane")
.lastName("Smith")
.build();
when(userService.createUser(any(CreateUserRequest.class))).thenReturn(response);
// when & then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.email").value("[email protected]"));
verify(userService).createUser(any(CreateUserRequest.class));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
// given
when(userService.getUserById(999L))
.thenThrow(new UserNotFoundException("User not found"));
// when & then
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("User not found"));
}
@Test
void shouldValidateCreateUserRequest() throws Exception {
// given - invalid request
CreateUserRequest invalidRequest = CreateUserRequest.builder()
.email("invalid-email")  // invalid email
.firstName("")  // empty first name
.build();
// when & then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalidRequest)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray());
}
}
// Controller class
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Validated
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@PostMapping
public ResponseEntity<UserResponse> createUser(
@Valid @RequestBody CreateUserRequest request) {
UserResponse response = userService.createUser(request);
return ResponseEntity.created(URI.create("/api/users/" + response.getId()))
.body(response);
}
@PutMapping("/{id}")
public UserResponse updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.updateUser(id, request);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
// DTO classes
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class CreateUserRequest {
@NotBlank
@Email
private String email;
@NotBlank
@Size(min = 2, max = 50)
private String firstName;
@NotBlank
@Size(min = 2, max = 50)
private String lastName;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class UserResponse {
private Long id;
private String email;
private String firstName;
private String lastName;
private LocalDateTime createdAt;
}

@WebMvcTest with Security

@WebMvcTest(UserController.class)
@WithMockUser
class UserControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
@WithMockUser(roles = "USER")
void shouldAllowUserAccess() throws Exception {
when(userService.getUserById(1L)).thenReturn(new UserResponse());
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminAccess() throws Exception {
when(userService.getUserById(1L)).thenReturn(new UserResponse());
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk());
}
@Test
void shouldDenyAccessWithoutAuthentication() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyAccessForForbiddenEndpoint() throws Exception {
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isForbidden());
}
}

@WebFluxTest - Reactive Controller Testing

import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.reactive.server.WebTestClient;
@WebFluxTest(UserReactiveController.class)
class UserReactiveControllerTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private UserReactiveService userService;
@Test
void shouldGetUser() {
// given
UserResponse userResponse = UserResponse.builder()
.id(1L)
.email("[email protected]")
.firstName("Reactive")
.lastName("User")
.build();
when(userService.getUserById(1L)).thenReturn(Mono.just(userResponse));
// when & then
webTestClient.get().uri("/api/reactive/users/1")
.exchange()
.expectStatus().isOk()
.expectBody(UserResponse.class)
.value(response -> {
assertThat(response.getEmail()).isEqualTo("[email protected]");
assertThat(response.getFirstName()).isEqualTo("Reactive");
});
}
@Test
void shouldStreamUsers() {
// given
UserResponse user1 = UserResponse.builder().id(1L).email("[email protected]").build();
UserResponse user2 = UserResponse.builder().id(2L).email("[email protected]").build();
when(userService.getAllUsers()).thenReturn(Flux.just(user1, user2));
// when & then
webTestClient.get().uri("/api/reactive/users/stream")
.accept(MediaType.TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.expectBodyList(UserResponse.class)
.hasSize(2);
}
}
@RestController
@RequestMapping("/api/reactive/users")
@RequiredArgsConstructor
public class UserReactiveController {
private final UserReactiveService userService;
@GetMapping("/{id}")
public Mono<UserResponse> getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@GetMapping("/stream")
public Flux<UserResponse> streamUsers() {
return userService.getAllUsers().delayElements(Duration.ofMillis(100));
}
}

JSON Layer Testing

@JsonTest - JSON Serialization/Deserialization Testing

import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
@JsonTest
class UserJsonTest {
@Autowired
private JacksonTester<UserResponse> json;
@Autowired
private JacksonTester<CreateUserRequest> requestJson;
@Test
void shouldSerializeUserResponse() throws Exception {
// given
UserResponse user = UserResponse.builder()
.id(1L)
.email("[email protected]")
.firstName("John")
.lastName("Doe")
.createdAt(LocalDateTime.of(2023, 1, 1, 10, 0))
.build();
// when & then
assertThat(json.write(user)).isEqualToJson("expected-user.json");
assertThat(json.write(user)).hasJsonPathStringValue("@.email");
assertThat(json.write(user)).extractingJsonPathStringValue("@.email")
.isEqualTo("[email protected]");
}
@Test
void shouldDeserializeUserResponse() throws Exception {
// given
String content = """
{
"id": 1,
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"createdAt": "2023-01-01T10:00:00"
}
""";
// when & then
assertThat(json.parse(content))
.isEqualTo(UserResponse.builder()
.id(1L)
.email("[email protected]")
.firstName("John")
.lastName("Doe")
.createdAt(LocalDateTime.of(2023, 1, 1, 10, 0))
.build());
assertThat(json.parseObject(content).getEmail()).isEqualTo("[email protected]");
}
@Test
void shouldDeserializeCreateUserRequest() throws Exception {
// given
String content = """
{
"email": "[email protected]",
"firstName": "Jane",
"lastName": "Smith"
}
""";
// when & then
assertThat(requestJson.parse(content))
.isEqualTo(CreateUserRequest.builder()
.email("[email protected]")
.firstName("Jane")
.lastName("Smith")
.build());
}
@Test
void shouldFailDeserializationWithInvalidEmail() throws Exception {
// given
String invalidContent = """
{
"email": "invalid-email",
"firstName": "John",
"lastName": "Doe"
}
""";
// when & then
assertThatThrownBy(() -> requestJson.parse(invalidContent))
.isInstanceOf(JsonParseException.class);
}
}
// expected-user.json
{
"id": 1,
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"createdAt": "2023-01-01T10:00:00"
}

Service Layer Testing

Mocking Dependencies in Service Tests

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserService userService;
@Test
void shouldCreateUserSuccessfully() {
// given
CreateUserRequest request = CreateUserRequest.builder()
.email("[email protected]")
.firstName("John")
.lastName("Doe")
.build();
User user = User.builder()
.id(1L)
.email("[email protected]")
.firstName("John")
.lastName("Doe")
.build();
when(userRepository.save(any(User.class))).thenReturn(user);
when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword");
doNothing().when(emailService).sendWelcomeEmail(anyString());
// when
UserResponse response = userService.createUser(request);
// then
assertThat(response.getId()).isEqualTo(1L);
assertThat(response.getEmail()).isEqualTo("[email protected]");
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("[email protected]");
}
@Test
void shouldThrowExceptionWhenEmailExists() {
// given
CreateUserRequest request = CreateUserRequest.builder()
.email("[email protected]")
.firstName("John")
.lastName("Doe")
.build();
when(userRepository.findByEmail("[email protected]"))
.thenReturn(new User());
// when & then
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(DuplicateEmailException.class)
.hasMessage("Email already exists: [email protected]");
verify(userRepository, never()).save(any(User.class));
}
@Test
void shouldUpdateUser() {
// given
Long userId = 1L;
UpdateUserRequest request = UpdateUserRequest.builder()
.firstName("Updated")
.lastName("Name")
.build();
User existingUser = User.builder()
.id(userId)
.email("[email protected]")
.firstName("Original")
.lastName("User")
.build();
User updatedUser = User.builder()
.id(userId)
.email("[email protected]")
.firstName("Updated")
.lastName("Name")
.build();
when(userRepository.findById(userId)).thenReturn(Optional.of(existingUser));
when(userRepository.save(any(User.class))).thenReturn(updatedUser);
// when
UserResponse response = userService.updateUser(userId, request);
// then
assertThat(response.getFirstName()).isEqualTo("Updated");
assertThat(response.getLastName()).isEqualTo("Name");
verify(userRepository).save(existingUser);
}
@Test
void shouldThrowExceptionWhenUserNotFoundForUpdate() {
// given
Long userId = 999L;
UpdateUserRequest request = UpdateUserRequest.builder().build();
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// when & then
assertThatThrownBy(() -> userService.updateUser(userId, request))
.isInstanceOf(UserNotFoundException.class)
.hasMessage("User not found with id: 999");
}
}

Configuration Testing

@Import and Test Configuration

@WebMvcTest(UserController.class)
@Import({UserService.class, SecurityConfig.class})
class UserControllerWithImportsTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserRepository userRepository;
@MockBean
private EmailService emailService;
@Test
void shouldWorkWithImportedConfigurations() throws Exception {
// Test implementation
}
}
@TestConfiguration
class TestServiceConfiguration {
@Bean
@Primary
public UserService testUserService() {
return new TestUserService();
}
@Bean
public PasswordEncoder testPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
@WebMvcTest(UserController.class)
@Import(TestServiceConfiguration.class)
class UserControllerWithTestConfigurationTest {
@Autowired
private UserService userService; // Uses the test bean
@Test
void shouldUseTestConfiguration() {
// Test implementation
}
}

Custom Test Slices

Creating Custom Test Slices

// Custom annotation for service layer testing
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(ServiceTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureMetrics
@ServiceTest
public @interface ServiceSliceTest {
@PropertyMapping("spring.test.service.enabled")
boolean enabled() default true;
Class<?>[] services() default {};
}
// Custom type exclude filter
class ServiceTypeExcludeFilter extends TypeExcludeFilter {
private final ServiceSliceTest annotation;
ServiceTypeExcludeFilter(BeanFactory beanFactory, ServiceSliceTest annotation) {
this.annotation = annotation;
}
@Override
public boolean match(MetadataReader metadataReader, 
MetadataReaderFactory metadataReaderFactory) throws IOException {
// Custom exclusion logic for services
return !isServiceComponent(metadataReader) && 
!isConfiguration(metadataReader);
}
private boolean isServiceComponent(MetadataReader metadataReader) {
// Implementation to detect service components
return metadataReader.getClassMetadata().getClassName().contains("Service");
}
}
// Auto-configuration for service slice
@AutoConfiguration
@ConditionalOnWebApplication
@Import(ServiceSliceTestConfiguration.class)
public class ServiceSliceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public UserService userService() {
return new UserService();
}
}
// Using the custom slice
@ServiceSliceTest(services = {UserService.class, EmailService.class})
class CustomServiceSliceTest {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
@Test
void shouldUseCustomSlice() {
// Test with custom slice configuration
}
}

Integration Test Slices

@SpringBootTest with Test Slices

@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"spring.jpa.hibernate.ddl-auto=create-drop"
}
)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
@TestPropertySource(locations = "classpath:application-test.properties")
class UserIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@LocalServerPort
private int port;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void shouldCreateUserThroughAllLayers() {
// given
CreateUserRequest request = CreateUserRequest.builder()
.email("[email protected]")
.firstName("Integration")
.lastName("Test")
.build();
// when
ResponseEntity<UserResponse> response = restTemplate.postForEntity(
"http://localhost:" + port + "/api/users",
request,
UserResponse.class
);
// then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getEmail()).isEqualTo("[email protected]");
// Verify database
assertThat(userRepository.count()).isEqualTo(1);
assertThat(userRepository.findByEmail("[email protected]")).isNotNull();
}
@Test
void shouldGetUserThroughAllLayers() {
// given
User user = User.builder()
.email("[email protected]")
.firstName("Get")
.lastName("User")
.build();
User saved = userRepository.save(user);
// when
ResponseEntity<UserResponse> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/users/" + saved.getId(),
UserResponse.class
);
// then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getEmail()).isEqualTo("[email protected]");
}
}

Performance Testing with Slices

Benchmarking Test Slices

class TestSlicePerformanceTest {
@Test
void compareTestSliceStartupTimes() {
// Measure @DataJpaTest startup time
long startTime = System.currentTimeMillis();
try (ConfigurableApplicationContext context = 
new SpringApplicationBuilder(DataJpaTestConfig.class)
.run()) {
long dataJpaTime = System.currentTimeMillis() - startTime;
System.out.println("@DataJpaTest startup: " + dataJpaTime + "ms");
}
// Measure @WebMvcTest startup time
startTime = System.currentTimeMillis();
try (ConfigurableApplicationContext context = 
new SpringApplicationBuilder(WebMvcTestConfig.class)
.run()) {
long webMvcTime = System.currentTimeMillis() - startTime;
System.out.println("@WebMvcTest startup: " + webMvcTime + "ms");
}
// Measure @SpringBootTest startup time
startTime = System.currentTimeMillis();
try (ConfigurableApplicationContext context = 
new SpringApplicationBuilder(FullAppTestConfig.class)
.run()) {
long fullAppTime = System.currentTimeMillis() - startTime;
System.out.println("@SpringBootTest startup: " + fullAppTime + "ms");
}
}
@DataJpaTest
static class DataJpaTestConfig {}
@WebMvcTest
static class WebMvcTestConfig {}
@SpringBootTest
static class FullAppTestConfig {}
}

Best Practices

Test Organization and Structure

// Base test class for common configuration
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public abstract class BaseIntegrationTest {
@Container
protected static PostgreSQLContainer<?> postgres = 
new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@BeforeEach
void setUp() {
// Common setup logic
}
@AfterEach
void tearDown() {
// Common cleanup logic
}
}
// Repository tests
@DataJpaTest
@Import(TestConfiguration.class)
class UserRepositoryTest extends BaseIntegrationTest {
@Test
void repositorySpecificTest() {
// Test implementation
}
}
// Controller tests
@WebMvcTest(UserController.class)
@WithMockUser
class UserControllerTest {
@Test
void controllerSpecificTest() {
// Test implementation
}
}
// Service tests
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Test
void serviceSpecificTest() {
// Test implementation
}
}

Conclusion

Spring Boot Test Slices provide a powerful way to write focused, fast, and maintainable tests:

  • @DataJpaTest: For testing JPA components with an embedded database
  • @WebMvcTest: For testing MVC controllers with mocked dependencies
  • @WebFluxTest: For testing reactive web controllers
  • @JsonTest: For testing JSON serialization/deserialization
  • @DataMongoTest: For testing MongoDB components
  • @JdbcTest: For testing JDBC components

Key Benefits:

  • Faster Tests: Load only necessary components
  • Focused Testing: Test specific layers in isolation
  • Better Maintainability: Clear separation of concerns
  • Reduced Complexity: Simplified test configuration

When to Use Which Slice:

  • Use @DataJpaTest for repository layer testing
  • Use @WebMvcTest for web layer testing
  • Use @JsonTest for serialization testing
  • Use @SpringBootTest for full integration testing
  • Use custom slices for domain-specific testing needs

By leveraging test slices appropriately, you can create a comprehensive test pyramid with fast, reliable tests at every level of your application.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper