Mastering Unit Testing in Spring Boot with Mockito

Integrating a robust mocking framework into a Spring Boot ecosystem significantly streamlines the vaildation process. By isolating components and simulating external dependencies, developers can verify business logic without triggering actual database transactions, network calls, or complex service interactions. The following implementations demonstrate practical approaches for common testing scenarios.

Isolating Business Logic from Data Repositories

When a core service relies heavily on persistence layers, injecting a simulated repository allows precise control over the returned data structures.

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class InvoiceProcessorTest {

    @Mock
    private InvoiceDao invoiceStorage;

    @InjectMocks
    private PaymentValidator billingEngine;

    @Test
    void verifyInvoiceRetrieval() {
        Invoice dummyRecord = new Invoice(99L, "Q4-Consulting", BigDecimal.valueOf(4500.00));
        when(invoiceStorage.fetchByReference(99L)).thenReturn(Optional.of(dummyRecord));

        Invoice fetchedRecord = billingEngine.resolveInvoice(99L);

        assertThat(fetchedRecord.getDescription()).isEqualTo("Q4-Consulting");
    }
}

Validating REST Endpoint Behavior

Spring provides a specialized annotation to slice the context down to the web layer, pairing seamlessly with mock service beans.

@WebMvcTest(PaymentEndpoint.class)
@ExtendWith(SpringExtension.class)
public class PaymentEndpointValidation {

    @MockBean
    private SettlementGateway settlementGateway;

    @Autowired
    private MockMvc apiTester;

    @Test
    void executePaymentRequest() throws Exception {
        SettlementStatus pendingStatus = new SettlementStatus("TXN-55", "PROCESSING");
        when(settlementGateway.initiateTransfer(anyLong())).thenReturn(pendingStatus);

        apiTester.perform(get("/api/payments/55"))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.transactionId").value("TXN-55"));
    }
}

Simulating Fault Conditions

Ensuring the application gracefully handles missing data or connectivity failures is critical for production reliability.

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class FaultHandlingValidation {

    @Mock
    private ExternalCreditBureau creditCheckClient;

    @InjectMocks
    private LoanApprovalSystem approvalSystem;

    @Test
    void handleMissingCreditProfile() {
        when(creditCheckClient.retrieveScore("USR-01")).thenThrow(new RuntimeException("Connection refused"));

        assertThrows(ServiceUnavailable.class, () -> {
            approvalSystem.evaluateApplication("USR-01");
        });
    }
}

Mocking Outbound HTTP Requests

When services consume third-party APIs, intercepting the HTTP client prevents actual network traffic during test execution.

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class GeolocationClientTest {

    @Mock
    private RestTemplate networkClient;

    @InjectMocks
    private LocationResolver resolverService;

    @Test
    void resolveCoordinatesFromExternalApi() {
        String mockPayload = "{\\"latitude\\":40.71,\\"longitude\\":-74.00}";
        when(networkClient.getForObject("https://maps.api/loc", String.class)).thenReturn(mockPayload);

        String resultPayload = resolverService.fetchCoordinates();

        assertThat(resultPayload).contains("40.71");
    }
}

Intercepting Method Arguments

Verifying that downstream components receive correctly transformed data requires argument capturing mechanisms.

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class DataSyncValidator {

    @Mock
    private CloudStorageAdapter storageConnector;

    @InjectMocks
    private BackupScheduler syncManager;

    @Test
    void verifyPayloadBeforeUpload() {
        Document report = new Document("annual-report", "PDF", 2048);
        syncManager.scheduleBackup(report);

        ArgumentCaptor<Document> capturedPayload = ArgumentCaptor.forClass(Document.class);
        verify(storageConnector).uploadDocument(capturedPayload.capture());
        Document verifiedDoc = capturedPayload.getValue();

        assertThat(verifiedDoc.getFormat()).isEqualTo("PDF");
    }
}

Overriding Static Utility Calls

Modern testing libraries support the interception of static method invocations, which is essential when working with legacy or tightly coupled utility classes.

@SpringBootTest
public class SecurityTokenGeneratorTest {

    @Test
    void interceptStaticEncryption() {
        try (MockedStatic<CryptoHelper> mockedHelper = Mockito.mockStatic(CryptoHelper.class)) {
            mockedHelper.when(() -> CryptoHelper.generateHash("raw_input")).thenReturn("SECURE_HASH_99");

            String token = AuthManager.createSecureToken("raw_input");

            assertThat(token).startsWith("SECURE");
        }
    }
}

Defining Sequential Return Values

Stateful testing often requires a mock to return different outcomes across repeated invocations.

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class ResourcePoolManagerTest {

    @Mock
    private ConnectionValidator healthMonitor;

    @InjectMocks
    private PoolAllocator allocator;

    @Test
    void simulateConnectionFluctuation() {
        when(healthMonitor.isActive(42L)).thenReturn(true, false, true);

        assertTrue(allocator.allocateSlot(42L));
        assertFalse(allocator.allocateSlot(42L));
        assertTrue(allocator.allocateSlot(42L));
    }
}

Utilizing Flexible Parameter Matchers

When exact argument values are irrelevant, matcher constraints simplify verification logic.

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class AlertDispatcherTest {

    @Mock
    private PushNotificationClient mobileClient;

    @InjectMocks
    private SystemMonitor monitor;

    @Test
    void triggerCriticalAlert() {
        monitor.raiseAlert("DB_CONNECTION_LOST", Severity.CRITICAL);

        verify(mobileClient).dispatchMessage(anyString(), eq(Severity.CRITICAL));
    }
}

Managing Void Method Behavior

Operations that produce side effects rather than return values require explicit stubbing directives.

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class AuditTrailIntegrationTest {

    @Mock
    private ComplianceRecorder complianceSystem;

    @InjectMocks
    private AccountManager userAdmin;

    @Test
    void logAdministrativeAction() {
        doNothing().when(complianceSystem).recordEvent(anyString());

        userAdmin.deactivateAccount("USR-77");

        verify(complianceSystem).recordEvent(contains("Account deactivation"));
    }
}

Handling Generic Method Signatures

Type-erased or generic interfaces can be safely mocked using wildcard matchers to satisfy compiler constraints.

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class SessionCacheTest {

    @Mock
    private CacheProvider<String, SessionData> sessionStore;

    @InjectMocks
    private AuthenticationFlow loginManager;

    @Test
    void retrieveCachedSession() {
        SessionData cachedInfo = new SessionData("active", 3600);
        when(sessionStore.lookup(any(), any())).thenReturn(cachedInfo);

        SessionData result = loginManager.getActiveSession("SID-001");

        assertThat(result.getStatus()).isEqualTo("active");
    }
}

Partial Mocking with Spies

When testing real instances but needing to override specific methods, a spy wraps the actual object while delegating unstubbed calls to the real implementation.

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class PricingStrategyTest {

    @Spy
    private BasePriceCalculator realCalculator = new BasePriceCalculator();

    @InjectMocks
    private FinalInvoiceGenerator generator;

    @Test
    void applyCustomDiscount() {
        OrderRequest cart = new OrderRequest(Arrays.asList("ITEM-A"), 200.0);
        doReturn(180.0).when(realCalculator).calculateBase(cart);

        double finalTotal = generator.computeTotal(cart);

        assertThat(finalTotal).isEqualTo(180.0);
    }
}

Enforcing Execution Sequence

Transactional or state-dependent logic often requires verifying that operations occur in a specific chronological order.

@SpringBootTest
@ExtendWith(MockitoExtension.class)
public class PaymentLifecycleTest {

    @Mock
    private LedgerWriter accountingModule;

    @InjectMocks
    private TransactionCoordinator flowController;

    @Test
    void verifyStepOrdering() {
        flowController.executeSettlement(new TransactionPayload("PAY-99", 500.0));

        InOrder sequenceVerifier = inOrder(accountingModule);
        sequenceVerifier.verify(accountingModule).lockFunds(anyLong());
        sequenceVerifier.verify(accountingModule).commitLedgerEntry(any());
    }
}

Tags: spring-boot Mockito JUnit5 java-testing mockmvc

Posted on Thu, 10 Sep 2026 16:50:34 +0000 by maff20