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;
    @MockitoBean 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;
    @MockitoBean 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@MockitoBeanReplace in context@MockitoBean 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 (@MockitoBean)
@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$;
    @MockitoBean $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;
    @MockitoBean $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 @MockitoBean 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 @MockitoBean

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 Part 2: Context Management & Performance Secrets

Now that you’ve mastered the 70-20-10 pyramid and smart mocking patterns, you’re ready to tackle the #1 cause of slow test suites: poor context management.

  • Context Caching - How Spring reuses contexts and why your tests might be creating new ones
  • @DirtiesContext Alternatives - Fast cleanup strategies that don’t kill performance
  • Configuration Inheritance - Building a test class hierarchy for maximum context reuse
  • Parallel Execution - Configuring Maven/Gradle for 2-4x faster test suites
  • Performance Optimization - Proven patterns to achieve 10x speed improvements

Essential Documentation

Companion Content


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

Related Posts

Javadoc Best Practices for Spring Boot - 5-Minute Professional Guide 2025

You’ve written clean code. Your tests pass. Your PR is ready. Then comes the review comment: “Please add Javadoc.”

Sound familiar?

Read more

Spring Boot Testing Strategy – Context Management & Perf Secrets (Part 2)

Every second saved in test execution multiplies across your entire team and CI/CD pipeline. A 10x improvement in test performance can save hours of developer time daily—transforming a 10-minute test suite into a 1-minute feedback loop.

Read more