In high-stakes logic like a ShoppingCart checkout, it isn't enough to just check the return value. You need to ensure the PaymentService was actually contacted and that the internal list was modified correctly. This project demonstrates how to use Mocks for external dependencies and Spies for internal state tracking.
This is a critical distinction in advanced testing:
@Mock(PaymentService): A complete "dummy" object. It has no real logic. If you don't stub a method, it returnsnullorfalse.@Spy(List): A "partial mock." It wraps a real object (ArrayList). It calls the real methods (likeadd()) but allows you to track interactions and verify how many times those real methods were invoked.
Verification moves the test from "What did I get?" to "What did you do?".
verify(paymentService).processPayment(100): Ensures the code actually called the payment service with the specific amount.times(2): A verification mode that counts invocations. This is useful for ensuring loops or multi-step processes work as intended.- **
anyString()/anyInt()**: Argument matchers that make tests flexible when the exact value doesn't matter, only the type does.
By using ShoppingCart as the System Under Test (SUT), you’ve demonstrated how to verify interactions and handle edge cases like exceptions.
This diagram illustrates the internal state of the ShoppingCartTest. It highlights how Mockito manages the dependencies injected into the ShoppingCart.
Key Concepts:
@Mock(PaymentService): A completely hollow proxy. Unless you usewhen().thenReturn(), its methods do nothing (or return default values likefalse).@Spy(List): A realArrayListinstance. It actually stores the items ("Apple", "Banana"), but Mockito "wraps" it so you canverify()how many timesadd()was called.@InjectMocks(ShoppingCart): The real class we are testing. Mockito automatically passes the Mock and the Spy into its constructor.
This flow traces the testCheckoutSuccessful method, showing how the "trained" mock responds to the call.
sequenceDiagram
autonumber
participant Test as ShoppingCartTest
participant Cart as ShoppingCart (SUT)
participant SpyList as items (ArrayList Spy)
participant MockPay as paymentService (Mock)
Note over Test, MockPay: === Setup ===
Test->>MockPay: when(processPayment(100)).thenReturn(true)
Note over Test, MockPay: === Execution ===
Test->>SpyList: add("Apple")
Test->>SpyList: add("Banana")
Test->>Cart: checkout(100)
activate Cart
Cart->>SpyList: isEmpty()
SpyList-->>Cart: false (Real ArrayList logic)
Cart->>MockPay: processPayment(100)
activate MockPay
Note right of MockPay: Returns programmed value
MockPay-->>Cart: true
deactivate MockPay
Cart-->>Test: true
deactivate Cart
Note over Test, MockPay: === Verification ===
Test->>MockPay: verify processPayment(100) called?
Test->>SpyList: verify add() called 2 times?
| Annotation | Type | Behavior | Best Use Case |
|---|---|---|---|
@Mock |
Fake | Hollow proxy; returns null, 0, or false. |
External dependencies (Databases, APIs). |
@Spy |
Real + Tracking | Calls real methods unless stubbed. | Utilities or collections where internal state matters. |
@InjectMocks |
Real | Actual class being tested; dependencies injected. | The "System Under Test" (SUT). |
In your testCheckoutThrowsException method, you demonstrate Stubbing with Exceptions:
when(paymentService.processPayment(anyInt()))
.thenThrow(new RuntimeException("Connection Lost"));This is a critical part of robust testing. It ensures that if the PaymentService fails (network error, timeout, etc.), your ShoppingCart doesn't crash unexpectedly or, if it does, it's handled according to the business requirements.
Observation: In ShoppingCart.java, you don't have a try-catch block. This means the RuntimeException bubbles up directly to the caller, which is exactly what your assertThrows in the test verifies.
The "System Under Test" (SUT). It contains a guard clause (checking if the cart is empty) before delegating the heavy lifting to the PaymentService.
testCheckoutSuccessful: Demonstrates the full flow. It uses the Spy to track two item additions and the Mock to simulate a successful bank response.testCheckoutThrowsException: Demonstrates "Negative Testing." We simulate a network failure (RuntimeException) and verify that our cart doesn't try to handle it incorrectly, but instead bubbles the exception up.
- Interaction Testing: You aren't just testing the result; you are testing the contract between the ShoppingCart and the PaymentService.
- Real Object Tracking: Using a
@Spyon anArrayListallows you to maintain the real behavior of the list while still having "spy cameras" on its methods. - Error Simulation: Mockito makes it trivial to simulate rare errors (like "Connection Lost") that are otherwise very difficult to trigger in a live environment.
- Interface: Defines a contract but no implementation.
- Method Signature:
boolean processPayment(int amount)must be implemented by any class that uses this interface. - Return Type:
booleanindicates success (true) or failure (false). - Purpose: Perfect for mocking in tests (e.g., using Mockito) because you can simulate payment success or failure without connecting to a real payment system.
- Dependency Injection:
PaymentServiceis passed into the constructor, making it easy to replace with a mock in tests. - Validation: The cart must have items before checkout.
- Delegation: Payment responsibility is delegated to
PaymentService. - Return Value: Checkout returns
trueif payment succeeds,falseotherwise.
- Spring Boot Application: This is the "main class" that starts your project.
- @SpringBootApplication: Combines configuration, auto-setup, and component scanning.
- SpringApplication.run(): Actually starts the application and embedded server.
- Purpose: Without this class, your Spring Boot app won’t launch.
-
@SpringBootTest: Boots up the entire Spring context for testing.
-
contextLoads() Test: A simple test that ensures the application starts correctly.
-
Purpose: This is often the first test in a Spring Boot project to confirm basic setup.
MockitoTestingApplicationTests ✔ contextLoads ✔
- @Mock: Creates fake dependencies (here,
PaymentService). - @Spy: Wraps a real object but allows monitoring and partial mocking.
- @InjectMocks: Injects mocks/spies into the class under test (
ShoppingCart). - Mockito Behavior:
when(...).thenReturn(...): Defines mock return values.when(...).thenThrow(...): Defines mock exceptions.verify(...): Ensures methods were called with expected arguments.
- JUnit Assertions:
-
assertTrue,assertFalse,assertThrows: Verify expected outcomes.ShoppingCartTest ✔ testCheckoutSuccessful ✔ testCheckoutFailureDueToPayment ✔ testCheckoutThrowsException ✔
-
| Tool | Purpose | Use Case in Project |
|---|---|---|
@Mock |
Create a fake dependency | Simulating the PaymentService bank API. |
@Spy |
Wrap a real object | Tracking items added to the items list. |
verify() |
Check if a method was called | Ensuring the payment was actually triggered. |
anyInt() |
Flexible matching | Stubbing payments regardless of the specific amount. |