Spring Boot Testing Strategy – Ultimate Cheatsheet 2025 (Part 1)
Table of Contents
Most Spring Boot testing articles bury you in theory. This one is built for action: a battle-tested reference you can keep open while coding.
In this Part 1, you’ll get:
- The 70-20-10 testing pyramid that actually works
- Layer-by-layer testing strategies with code snippets
- The smart mocking framework for Spring Boot
- Planning techniques to design tests in minutes
- Quick-reference annotations, IntelliJ templates, fixes, and anti-patterns
Testing Strategy & Philosophy
The 70-20-10 Testing Pyramid
Layer | Allocation | Purpose | Speed | When to Use |
---|---|---|---|---|
Unit | 70% | Test logic in isolation | ⚡⚡⚡ | Business logic, calculations |
Integration | 20% | Verify components work together | ⚡⚡ | DB ops, API calls, workflows |
E2E/UI | 10% | Validate complete user journeys | ⚡ | Critical flows, UI interaction |
Why this works in Spring Boot:
- Unit tests → ms feedback, zero Spring startup
- Integration tests → catch wiring/config bugs
- E2E tests → ensure real-world flow integrity
Coverage Guidelines That Matter
✅ Aim: 80%+ branch coverage for business logic
❌ Avoid: chasing 100% on boilerplate
🔍 Tools: JaCoCo, IntelliJ Coverage, SonarQube, Pitest
Focus on:
- Branches & decision points, not just lines
- Skip trivial getters/setters
- Mutation testing > coverage % for real quality
Layer-by-Layer Testing Strategy
Quick Decision Framework
Which layer?
- Controller →
@WebMvcTest
+ mock services - Service → Unit test + mock repos
- Repository →
@DataJpaTest
- Full flow →
@SpringBootTest
Dependencies?
- Many externals → Unit test with mocks
- Few/simple → Integration possible
Goal?
- Speed → Unit
- Config check → Integration
- Real journey → E2E
Web/Controller Layer
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService;
@Test
void should_create_user_successfully() throws Exception {
when(userService.createUser(any())).thenReturn(createdUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "John", "email": "john@test.com"}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("John"));
}
}
💡 Best for: request/response mapping, validation, security.
Service/Business Layer
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository userRepository;
@Mock EmailService emailService;
@InjectMocks UserService service;
@Test
void should_create_user_and_send_email() {
when(userRepository.save(any())).thenReturn(savedUser);
var result = service.createUser(request);
assertThat(result.getId()).isNotNull();
verify(emailService).sendWelcomeEmail(savedUser.getEmail());
}
}
💡 Best for: core business logic, calculations, workflows.
Repository/Data Layer
@DataJpaTest
class UserRepositoryTest {
@Autowired UserRepository repo;
@Autowired TestEntityManager em;
@Test
void should_find_users_by_email_domain() {
em.persistAndFlush(new User("john@company.com"));
em.persistAndFlush(new User("jane@company.com"));
var users = repo.findByEmailDomain("company.com");
assertThat(users).hasSize(2);
}
}
💡 Best for: testing custom queries, verifying JPA mappings, checking DB constraints in isolation.
Full Integration
@SpringBootTest(webEnvironment = RANDOM_PORT)
class UserIntegrationTest {
@Autowired TestRestTemplate restTemplate;
@MockBean ExternalEmailService emailService;
@Test
void should_create_user_end_to_end() {
when(emailService.validateEmail(any())).thenReturn(true);
var res = restTemplate.postForEntity("/api/users",
new CreateUserRequest("John", "john@test.com"), User.class);
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(res.getBody().getId()).isNotNull();
}
}
💡 Best for: Full end-to-end workflows, validating Spring configuration, testing with external integrations (mocked).
Smart Mocking Strategy
@Mock vs @Spy vs mock() Decision Matrix
Scenario | Choose | Why | Example |
---|---|---|---|
Shared across multiple tests | @Mock | Clean, reusable | @Mock UserRepository repo; |
Need real behavior + spying | @Spy | Hybrid testing | @Spy ObjectMapper mapper; |
One-off dependency | mock() | Local scope | var mockService = mock(EmailService.class); |
Spring Boot bean replacement | @MockBean | Replace in context | @MockBean ExternalService service; |
@Spy Important Rules
java✅ Use on concrete classes (implementations), not interfaces
✅ Works with no-argument constructors automatically
✅ For parameterized constructors, initialize inline
// ✅ Good - concrete class with no-arg constructor
@Spy ObjectMapper objectMapper = new ObjectMapper();
@Spy ProductMapper mapper = Mappers.getMapper(ProductMapper.class);
// ✅ Good - inline initialization for parameterized constructor
@Spy ReportService reportService = new ReportService(config, validator);
// ❌ Bad - cannot spy on interfaces
@Spy UserService userService; // If UserService is interface
// ❌ Bad - parameterized constructor without initialization
@Spy EmailService emailService; // If EmailService(String apiKey) constructor
Why these rules matter:
Interfaces: Mockito needs real methods to spy on
Constructors: @Spy calls no-arg constructor by default
Parameterized: Must provide constructor arguments explicitly
Always Mock External/Unpredictable
// External systems (leave your JVM)
@Mock UserRepository userRepository; // Database calls
@Mock EmailService emailService; // External service
@Mock RestTemplate restTemplate; // HTTP calls
@Mock FileService fileService; // File I/O
// Unpredictable behavior
@Mock Clock clock; // Time dependencies
@Mock Random random; // Random values
Rule:
- If it crosses application boundaries → mock it
- If it changes between runs (time, randomness, environment) → mock it
Keep Real for Simple/Deterministic
// Pure utilities and mappers
@Spy ObjectMapper objectMapper = new ObjectMapper();
@Spy ProductMapper mapper = Mappers.getMapper(ProductMapper.class);
StringUtils stringUtils = new StringUtils();
ValidationUtils validator = new ValidationUtils();
Rule:
- If it’s deterministic → keep it real
- If it doesn’t cross boundaries → keep it real
5-Minute Test Planning – Path 1–3 Example
@ExtendWith(MockitoExtension.class)
class ReportServiceTest {
@Mock ExternalService externalService;
@Spy ObjectMapper objectMapper = new ObjectMapper();
@Spy ReportMapper reportMapper = Mappers.getMapper(ReportMapper.class);
@InjectMocks ReportService reportService;
private RepeatStatus handleStatus(FeedbackResponse response, String id) {
if (!response.hasError()) return FINISHED; // Path 1
try {
var file = externalService.getReport(id); // Path 2
return process(file);
} catch (Exception e) {
log.error("Failed", e);
return FINISHED; // Path 3
}
}
}
💡 Without @Spy, inject manually:
ReflectionTestUtils.setField(reportService, "reportMapper",
Mappers.getMapper(ReportMapper.class));
Test Path Table:
Path | Condition | Expected Result | Mocks Needed | Verify |
---|---|---|---|---|
1 | no error | FINISHED | None | No calls |
2 | error + success | process() result | externalService | getReport() called |
3 | error + exception | FINISHED + log | externalService | error logged |
Complete Annotation Reference
Annotation | Loads | Use Case | Typical Mocks |
---|---|---|---|
@WebMvcTest | Web layer only | Controller logic | Services (@MockBean ) |
@DataJpaTest | JPA components | Repository testing | External services |
@JsonTest | JSON serialization | DTO mapping | None |
@SpringBootTest | Full context | Integration testing | External systems |
@TestMethodOrder | N/A | Control execution order | N/A |
@DirtiesContext | N/A | Reset context | Use sparingly |
IntelliJ Live Templates for Instant Test Creation
(Settings → Editor → Live Templates → New group “Spring Tests”)
Unit Test – sunit
@ExtendWith(MockitoExtension.class)
class $CLASS_NAME$Test {
@Mock $DEPENDENCY$ $dependencyName$;
@InjectMocks $CLASS_NAME$ $instanceName$;
@Test
void should_$BEHAVIOR$_when_$CONDITION$() {
// Given
$GIVEN$
// When
$WHEN$
// Then
$THEN$
}
}
💡 Tip: Ctrl+Shift+T → auto-generate test skeleton.
Integration Test – sint
@SpringBootTest
class $CLASS_NAME$IntegrationTest {
@Autowired $SERVICE_CLASS$ $serviceName$;
@MockBean $EXTERNAL_SERVICE$ $externalService$;
@Test
void should_$BEHAVIOR$_end_to_end() {
// Given
when($externalService$.$method$(any())).thenReturn($returnValue$);
// When
$WHEN$
// Then
$THEN$
}
}
Web Test – sweb
@WebMvcTest($CONTROLLER_CLASS$.class)
class $CONTROLLER_CLASS$Test {
@Autowired MockMvc mockMvc;
@MockBean $SERVICE_CLASS$ $serviceName$;
@Test
void should_$BEHAVIOR$() throws Exception {
// Given
when($serviceName$.$method$(any())).thenReturn($returnValue$);
// When & Then
mockMvc.perform($HTTP_METHOD$("$ENDPOINT$")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().$EXPECTED_STATUS$());
}
}
Common Test Failures & Lightning Fixes
NullPointerException
- Add
@ExtendWith(MockitoExtension.class)
- Ensure
@Mock
present @InjectMocks
on class under test
Mocks Not Behaving
- Match args consistently
- Use
@MockBean
for Spring context
Flaky Tests
- Mock time dependencies
- Avoid shared static state
- Await async completion
Spring Context Issues
- Use slice tests (
@WebMvcTest
,@DataJpaTest
) - Add missing beans with
@MockBean
Testing Anti-Patterns to Avoid
❌ Testing implementation details instead of behavior
❌ Over-mocking simple objects
❌ Massive setup methods
❌ Multiple concerns in one test
❌ State-dependent test order
Focus on: behavior, keep tests small, independent, and readable.
Key Takeaways & What’s Next
Essential Principles from Part 1
- Follow 70-20-10 for optimal speed and confidence
- Mock externals, keep simple objects real
- Choose the right annotation for faster test execution
- Plan test paths before coding to save debugging time
- Use IntelliJ templates for instant test skeleton creation
Coming Up in This Series
- Part 2: Advanced Configuration & Test Slices
- Part 3: Database Testing & Transaction Management
- Part 4: Security Testing & MockMvc Patterns
- Part 5: Performance & Load Testing
📚 Resources & Links
🔗 Essential Documentation
🎥 Companion Content
Last updated: August 2025 | Spring Boot 3.x + Java 21+