Real World Example

Testing a Wallet Top-up Service (Paytm / PhonePe style)

The service below adds money to a wallet after the bank confirms the payment. The tests cover the happy path, a declined payment and a duplicate request — the cases that cost real money if they break.

@Service
public class TopUpService {
    private final WalletRepository wallets;
    private final BankClient bank;
    private final NotificationService notifications;

    public TopUpService(WalletRepository wallets, BankClient bank, NotificationService notifications) {
        this.wallets = wallets; this.bank = bank; this.notifications = notifications;
    }

    @Transactional
    public TopUpResult topUp(String userId, String requestId, BigDecimal amount) {
        if (wallets.existsTopUp(requestId)) return TopUpResult.DUPLICATE;      // idempotency
        if (bank.debit(userId, amount) != BankStatus.APPROVED) return TopUpResult.DECLINED;
        Wallet w = wallets.findByUserId(userId).orElseThrow();
        w.credit(amount);
        wallets.save(w);
        wallets.recordTopUp(requestId);
        notifications.push(userId, "Rs " + amount + " added to your wallet");
        return TopUpResult.SUCCESS;
    }
}

The Tests

@ExtendWith(MockitoExtension.class)
class TopUpServiceTest {
    @Mock WalletRepository wallets;
    @Mock BankClient bank;
    @Mock NotificationService notifications;
    @InjectMocks TopUpService service;

    @Test
    void creditsWalletWhenBankApproves() {
        Wallet wallet = new Wallet("u1", new BigDecimal("100"));
        when(wallets.existsTopUp("REQ-1")).thenReturn(false);
        when(bank.debit("u1", new BigDecimal("500"))).thenReturn(BankStatus.APPROVED);
        when(wallets.findByUserId("u1")).thenReturn(Optional.of(wallet));

        TopUpResult result = service.topUp("u1", "REQ-1", new BigDecimal("500"));

        assertEquals(TopUpResult.SUCCESS, result);
        assertEquals(new BigDecimal("600"), wallet.balance());
        verify(wallets).recordTopUp("REQ-1");
        verify(notifications).push("u1", "Rs 500 added to your wallet");
    }

    @Test
    void doesNotCreditWhenBankDeclines() {
        when(wallets.existsTopUp("REQ-2")).thenReturn(false);
        when(bank.debit(any(), any())).thenReturn(BankStatus.DECLINED);

        assertEquals(TopUpResult.DECLINED, service.topUp("u1", "REQ-2", BigDecimal.TEN));
        verify(wallets, never()).save(any());
        verifyNoInteractions(notifications);
    }

    @Test
    void duplicateRequestIsIgnored() {
        when(wallets.existsTopUp("REQ-1")).thenReturn(true);

        assertEquals(TopUpResult.DUPLICATE, service.topUp("u1", "REQ-1", BigDecimal.TEN));
        verifyNoInteractions(bank, notifications);            // the user is not charged twice
    }
}

Example

mvn test

Example output

[INFO] Running com.wallet.TopUpServiceTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.612 s
[INFO]
[INFO] Results:
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

Test Pyramid

▲   E2E / UI tests (few, slow)          Selenium, Playwright
       ▲▲▲  Integration tests                    @SpringBootTest, @DataJpaTest, Testcontainers
     ▲▲▲▲▲▲ Unit tests (many, fast)             JUnit 5 + Mockito  ← this module