Spring Boot Testing Strategy – Ultimate Cheatsheet 2025 (Part 1)

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

LayerAllocationPurposeSpeedWhen to Use
Unit70%Test logic in isolation⚡⚡⚡Business logic, calculations
Integration20%Verify components work together⚡⚡DB ops, API calls, workflows
E2E/UI10%Validate complete user journeysCritical 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

ScenarioChooseWhyExample
Shared across multiple tests@MockClean, reusable@Mock UserRepository repo;
Need real behavior + spying@SpyHybrid testing@Spy ObjectMapper mapper;
One-off dependencymock()Local scopevar mockService = mock(EmailService.class);
Spring Boot bean replacement@MockBeanReplace 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:

PathConditionExpected ResultMocks NeededVerify
1no errorFINISHEDNoneNo calls
2error + successprocess() resultexternalServicegetReport() called
3error + exceptionFINISHED + logexternalServiceerror logged

Complete Annotation Reference

AnnotationLoadsUse CaseTypical Mocks
@WebMvcTestWeb layer onlyController logicServices (@MockBean)
@DataJpaTestJPA componentsRepository testingExternal services
@JsonTestJSON serializationDTO mappingNone
@SpringBootTestFull contextIntegration testingExternal systems
@TestMethodOrderN/AControl execution orderN/A
@DirtiesContextN/AReset contextUse 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

  1. Follow 70-20-10 for optimal speed and confidence
  2. Mock externals, keep simple objects real
  3. Choose the right annotation for faster test execution
  4. Plan test paths before coding to save debugging time
  5. 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

🔗 Essential Documentation

🎥 Companion Content


Last updated: August 2025 | Spring Boot 3.x + Java 21+

Related Posts

Java Collections: Performance Optimization Guide 2025

Most Java Collections articles overwhelm you with theory. This one is built for performance: a battle-tested reference you can apply immediately to make your Java code faster.

Read more