- Spring Boot Testing Patterns
- Overview
- This skill provides comprehensive guidance for writing robust test suites for Spring Boot applications. It covers unit testing with Mockito, integration testing with Testcontainers, performance-optimized slice testing patterns, and best practices for maintaining fast feedback loops.
- When to Use This Skill
- Use this skill when:
- Writing unit tests for services, repositories, or utilities
- Implementing integration tests with real databases using Testcontainers
- Setting up performance-optimized test slices (@DataJpaTest, @WebMvcTest)
- Configuring Spring Boot 3.5+ @ServiceConnection for container management
- Testing REST APIs with MockMvc, TestRestTemplate, or WebTestClient
- Optimizing test performance through context caching and container reuse
- Setting up CI/CD pipelines for integration tests
- Implementing comprehensive test strategies for monolithic or microservices applications
- Core Concepts
- Test Architecture Philosophy
- Spring Boot testing follows a layered approach with distinct test types:
- 1. Unit Tests
- Fast, isolated tests without Spring context
- Use Mockito for dependency injection
- Focus on business logic validation
- Target completion time: < 50ms per test
- 2. Slice Tests
- Minimal Spring context loading for specific layers
- Use @DataJpaTest for repository tests
- Use @WebMvcTest for controller tests
- Use @WebFluxTest for reactive controller tests
- Target completion time: < 100ms per test
- 3. Integration Tests
- Full Spring context with real dependencies
- Use @SpringBootTest with @ServiceConnection containers
- Test complete application flows
- Target completion time: < 500ms per test
- Key Testing Annotations
- Spring Boot Test Annotations:
- @SpringBootTest
-
- Load full application context (use sparingly)
- @DataJpaTest
-
- Load only JPA components (repositories, entities)
- @WebMvcTest
-
- Load only MVC layer (controllers, @ControllerAdvice)
- @WebFluxTest
-
- Load only WebFlux layer (reactive controllers)
- @JsonTest
-
- Load only JSON serialization components
- Testcontainer Annotations:
- @ServiceConnection
-
- Wire Testcontainer to Spring Boot test (Spring Boot 3.5+)
- @DynamicPropertySource
-
- Register dynamic properties at runtime
- @Testcontainers
- Enable Testcontainers lifecycle management Dependencies Maven Dependencies < dependencies
< dependency
< groupId
org.springframework.boot </ groupId
< artifactId
spring-boot-starter-test </ artifactId
< scope
test </ scope
</ dependency
< dependency
< groupId
org.testcontainers </ groupId
< artifactId
junit-jupiter </ artifactId
< version
1.19.0 </ version
< scope
test </ scope
</ dependency
< dependency
< groupId
org.testcontainers </ groupId
< artifactId
postgresql </ artifactId
< version
1.19.0 </ version
< scope
test </ scope
</ dependency
< dependency
< groupId
org.springframework.boot </ groupId
< artifactId
spring-boot-starter-data-jpa </ artifactId
</ dependency
< dependency
< groupId
org.springframework.boot </ groupId
< artifactId
spring-boot-starter-web </ artifactId
</ dependency
</ dependencies
Gradle Dependencies dependencies { // Spring Boot Test Starter testImplementation ( "org.springframework.boot:spring-boot-starter-test" ) // Testcontainers testImplementation ( "org.testcontainers:junit-jupiter:1.19.0" ) testImplementation ( "org.testcontainers:postgresql:1.19.0" ) // Additional Dependencies implementation ( "org.springframework.boot:spring-boot-starter-data-jpa" ) implementation ( "org.springframework.boot:spring-boot-starter-web" ) } Instructions Unit Testing Pattern Test business logic with mocked dependencies: class UserServiceTest { @Mock private UserRepository userRepository ; @InjectMocks private UserService userService ; @BeforeEach void setUp ( ) { MockitoAnnotations . openMocks ( this ) ; } @Test void shouldFindUserByIdWhenExists ( ) { // Arrange Long userId = 1L ; User user = new User ( ) ; user . setId ( userId ) ; user . setEmail ( "test@example.com" ) ; when ( userRepository . findById ( userId ) ) . thenReturn ( Optional . of ( user ) ) ; // Act Optional < User
result
userService . findById ( userId ) ; // Assert assertThat ( result ) . isPresent ( ) ; assertThat ( result . get ( ) . getEmail ( ) ) . isEqualTo ( "test@example.com" ) ; verify ( userRepository , times ( 1 ) ) . findById ( userId ) ; } } Slice Testing Pattern Use focused test slices for specific layers: // Repository test with minimal context @DataJpaTest @AutoConfigureTestDatabase ( replace = AutoConfigureTestDatabase . Replace . NONE ) @TestContainerConfig public class UserRepositoryIntegrationTest { @Autowired private UserRepository userRepository ; @Test void shouldSaveAndRetrieveUserFromDatabase ( ) { // Arrange User user = new User ( ) ; user . setEmail ( "test@example.com" ) ; user . setName ( "Test User" ) ; // Act User saved = userRepository . save ( user ) ; userRepository . flush ( ) ; Optional < User
retrieved
userRepository . findByEmail ( "test@example.com" ) ; // Assert assertThat ( retrieved ) . isPresent ( ) ; assertThat ( retrieved . get ( ) . getName ( ) ) . isEqualTo ( "Test User" ) ; } } REST API Testing Pattern Test controllers with MockMvc for faster execution: @SpringBootTest @AutoConfigureMockMvc @Transactional public class UserControllerIntegrationTest { @Autowired private MockMvc mockMvc ; @Autowired private ObjectMapper objectMapper ; @Autowired private UserService userService ; @Test void shouldCreateUserAndReturn201 ( ) throws Exception { User user = new User ( ) ; user . setEmail ( "newuser@example.com" ) ; user . setName ( "New User" ) ; mockMvc . perform ( post ( "/api/users" ) . contentType ( MediaType . APPLICATION_JSON ) . content ( objectMapper . writeValueAsString ( user ) ) ) . andExpect ( status ( ) . isCreated ( ) ) . andExpect ( jsonPath ( "$.id" ) . exists ( ) ) . andExpect ( jsonPath ( "$.email" ) . value ( "newuser@example.com" ) ) . andExpect ( jsonPath ( "$.name" ) . value ( "New User" ) ) ; } } Testcontainers with @ServiceConnection Configure containers with Spring Boot 3.5+: @TestConfiguration public class TestContainerConfig { @Bean @ServiceConnection public PostgreSQLContainer < ?
postgresContainer ( ) { return new PostgreSQLContainer <
( DockerImageName . parse ( "postgres:16-alpine" ) ) . withDatabaseName ( "testdb" ) . withUsername ( "test" ) . withPassword ( "test" ) ; } } Examples Basic Unit Test @Test void shouldCalculateTotalPrice ( ) { // Arrange OrderItem item1 = new OrderItem ( ) ; item1 . setPrice ( 10.0 ) ; item1 . setQuantity ( 2 ) ; OrderItem item2 = new OrderItem ( ) ; item2 . setPrice ( 15.0 ) ; item2 . setQuantity ( 1 ) ; List < OrderItem
items
List . of ( item1 , item2 ) ; // Act double total = orderService . calculateTotal ( items ) ; // Assert assertThat ( total ) . isEqualTo ( 35.0 ) ; } Integration Test with Testcontainers @SpringBootTest @TestContainerConfig public class OrderServiceIntegrationTest { @Autowired private OrderService orderService ; @Autowired private UserRepository userRepository ; @MockBean private PaymentService paymentService ; @Test void shouldCreateOrderWithRealDatabase ( ) { // Arrange User user = new User ( ) ; user . setEmail ( "customer@example.com" ) ; user . setName ( "John Doe" ) ; User savedUser = userRepository . save ( user ) ; OrderRequest request = new OrderRequest ( ) ; request . setUserId ( savedUser . getId ( ) ) ; request . setItems ( List . of ( new OrderItemRequest ( 1L , 2 ) , new OrderItemRequest ( 2L , 1 ) ) ) ; when ( paymentService . processPayment ( any ( ) ) ) . thenReturn ( true ) ; // Act OrderResponse response = orderService . createOrder ( request ) ; // Assert assertThat ( response . getOrderId ( ) ) . isNotNull ( ) ; assertThat ( response . getStatus ( ) ) . isEqualTo ( "COMPLETED" ) ; verify ( paymentService , times ( 1 ) ) . processPayment ( any ( ) ) ; } } Reactive Test Pattern @SpringBootTest ( webEnvironment = SpringBootTest . WebEnvironment . RANDOM_PORT ) @AutoConfigureWebTestClient public class ReactiveUserControllerIntegrationTest { @Autowired private WebTestClient webTestClient ; @Test void shouldReturnUserAsJsonReactive ( ) { // Arrange User user = new User ( ) ; user . setEmail ( "reactive@example.com" ) ; user . setName ( "Reactive User" ) ; // Act & Assert webTestClient . get ( ) . uri ( "/api/users/1" ) . exchange ( ) . expectStatus ( ) . isOk ( ) . expectBody ( ) . jsonPath ( "$.email" ) . isEqualTo ( "reactive@example.com" ) . jsonPath ( "$.name" ) . isEqualTo ( "Reactive User" ) ; } } Best Practices 1. Choose the Right Test Type Select appropriate test annotations based on scope: // Use @DataJpaTest for repository-only tests (fastest) @DataJpaTest public class UserRepositoryTest { } // Use @WebMvcTest for controller-only tests @WebMvcTest ( UserController . class ) public class UserControllerTest { } // Use @SpringBootTest only for full integration testing @SpringBootTest public class UserServiceFullIntegrationTest { } 2. Use @ServiceConnection for Container Management Prefer @ServiceConnection over manual @DynamicPropertySource for cleaner code: // Good - Spring Boot 3.5+ @TestConfiguration public class TestConfig { @Bean @ServiceConnection public PostgreSQLContainer < ?
postgres ( ) { return new PostgreSQLContainer <
( DockerImageName . parse ( "postgres:16-alpine" ) ) ; } } 3. Keep Tests Deterministic Always initialize test data explicitly: // Good - Explicit setup @BeforeEach void setUp ( ) { userRepository . deleteAll ( ) ; User user = new User ( ) ; user . setEmail ( "test@example.com" ) ; userRepository . save ( user ) ; } // Avoid - Depending on other tests @Test void testUserExists ( ) { // Assumes previous test created a user Optional < User
user
userRepository . findByEmail ( "test@example.com" ) ; assertThat ( user ) . isPresent ( ) ; } 4. Use Meaningful Assertions Leverage AssertJ for readable, fluent assertions: // Good - Clear, readable assertions assertThat ( user . getEmail ( ) ) . isEqualTo ( "test@example.com" ) ; assertThat ( users ) . hasSize ( 3 ) . contains ( expectedUser ) ; // Avoid - JUnit assertions assertEquals ( "test@example.com" , user . getEmail ( ) ) ; assertTrue ( users . size ( ) == 3 ) ; 5. Organize Tests by Layer Group related tests in separate classes to optimize context caching: // Repository tests (uses @DataJpaTest) public class UserRepositoryTest { } // Controller tests (uses @WebMvcTest) public class UserControllerTest { } // Service tests (uses mocks, no context) public class UserServiceTest { } // Full integration tests (uses @SpringBootTest) public class UserFullIntegrationTest { } Performance Optimization Context Caching Strategy Maximize Spring context caching by grouping tests with similar configurations: // Group repository tests with same configuration @DataJpaTest @AutoConfigureTestDatabase ( replace = AutoConfigureTestDatabase . Replace . NONE ) @TestContainerConfig @TestPropertySource ( properties = "spring.datasource.url=jdbc:postgresql:testdb" ) public class UserRepositoryTest { } // Group controller tests with same configuration @WebMvcTest ( UserController . class ) @AutoConfigureMockMvc public class UserControllerTest { } Container Reuse Strategy Reuse Testcontainers at JVM level for better performance: @Testcontainers public class ContainerConfig { static final PostgreSQLContainer < ?
POSTGRES
new PostgreSQLContainer <
( DockerImageName . parse ( "postgres:16-alpine" ) ) . withDatabaseName ( "testdb" ) . withUsername ( "test" ) . withPassword ( "test" ) ; @BeforeAll static void startAll ( ) { POSTGRES . start ( ) ; } @AfterAll static void stopAll ( ) { POSTGRES . stop ( ) ; } } Test Execution Maven Test Execution
Run all tests
./mvnw test
Run specific test class
./mvnw test -Dtest = UserServiceTest
Run integration tests only
./mvnw test -Dintegration-test = true
Run tests with coverage
./mvnw clean jacoco:prepare-agent test jacoco:report Gradle Test Execution
Run all tests
./gradlew test
Run specific test class
./gradlew test --tests UserServiceTest
Run integration tests only
./gradlew integrationTest
Run tests with coverage
./gradlew test jacocoTestReport CI/CD Configuration GitHub Actions Example name : Spring Boot Tests on : [ push , pull_request ] jobs : test : runs-on : ubuntu - latest services : postgres : image : postgres : 16 - alpine env : POSTGRES_PASSWORD : test POSTGRES_USER : test POSTGRES_DB : testdb options :
-
- health - cmd pg_isready - - health - interval 10s - - health - timeout 5s - - health - retries 5 steps : - uses : actions/checkout@v3 - name : Set up JDK 17 uses : actions/setup - java@v3 with : java-version : '17' distribution : 'temurin' - name : Cache Maven dependencies uses : actions/cache@v3 with : path : ~/.m2/repository key : $ { { runner.os } } - maven - $ { { hashFiles(' **/pom.xml') } } restore-keys : $ { { runner.os } } - maven - - name : Run tests run : ./mvnw test - Dspring.profiles.active=test Docker Compose for Local Testing version : '3.8' services : postgres : image : postgres : 16 - alpine environment : POSTGRES_DB : testdb POSTGRES_USER : test POSTGRES_PASSWORD : test ports : - "5432:5432" volumes : - postgres_data : /var/lib/postgresql/data volumes : postgres_data : References For detailed information, refer to the following resources: API Reference - Complete test annotations and utilities Best Practices - Testing patterns and optimization Workflow Patterns - Complete integration test examples