Testing with Mockito
Master unit testing with mocks, stubs, and behavior verification
What You Will Learn
Testing is essential for reliable software. In this practical work, you'll master:
- Mocking - Creating fake objects that simulate real dependencies
- Stubbing - Defining what mocks should return
- Verification - Checking that methods were called correctly
- ArgumentCaptor - Capturing arguments for inspection
- @Spy - Partial mocking of real objects
- BDD Style - Given/When/Then testing pattern
Understanding Mocking
Imagine testing a car's speedometer. You don't need to actually drive at 100 mph - you just need to simulate the wheel spinning fast. A mock is a fake object that simulates a real dependency.
Real Code:
┌─────────────────┐ ┌─────────────────┐
│ OrderService │───────▶│ PaymentGateway │ (calls real bank API)
│ │ │ (external API) │
└─────────────────┘ └─────────────────┘
Unit Test with Mock:
┌─────────────────┐ ┌─────────────────┐
│ OrderService │───────▶│ Mock Gateway │ (returns fake success)
│ (tested code) │ │ (controlled) │
└─────────────────┘ └─────────────────┘
Benefits:
- Fast tests (no network calls)
- Reliable tests (no external failures)
- Control test scenarios (success, failure, timeout)
Prerequisites
- Java 17+ and Maven installed
- Basic JUnit 5 knowledge
- Understanding of dependency injection
Part 1: Project Setup
Step 1.1: Create Project Structure
mockito-lab/
├── pom.xml
└── src/
├── main/java/com/example/shop/
│ ├── model/
│ ├── repository/
│ └── service/
└── test/java/com/example/shop/
└── service/
Step 1.2: Create pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mockito-lab</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- (#1:JUnit 5) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<!-- (#2:Mockito Core) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- (#3:Mockito JUnit 5 Extension) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>
Part 2: Create Business Logic
Step 2.1: Create Models
Create src/main/java/com/example/shop/model/Order.java:
package com.example.shop.model;
import java.math.BigDecimal;
import java.util.List;
public class Order {
private Long id;
private Long customerId;
private List<OrderItem> items;
private OrderStatus status;
private BigDecimal total;
public Order(Long id, Long customerId, List<OrderItem> items) {
this.id = id;
this.customerId = customerId;
this.items = items;
this.status = OrderStatus.PENDING;
this.total = items.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Getters and Setters
public Long getId() { return id; }
public Long getCustomerId() { return customerId; }
public List<OrderItem> getItems() { return items; }
public OrderStatus getStatus() { return status; }
public void setStatus(OrderStatus status) { this.status = status; }
public BigDecimal getTotal() { return total; }
}
Create src/main/java/com/example/shop/model/OrderItem.java:
package com.example.shop.model;
import java.math.BigDecimal;
public class OrderItem {
private Long productId;
private String productName;
private int quantity;
private BigDecimal price;
public OrderItem(Long productId, String productName, int quantity, BigDecimal price) {
this.productId = productId;
this.productName = productName;
this.quantity = quantity;
this.price = price;
}
public Long getProductId() { return productId; }
public String getProductName() { return productName; }
public int getQuantity() { return quantity; }
public BigDecimal getPrice() { return price; }
}
Create src/main/java/com/example/shop/model/OrderStatus.java:
package com.example.shop.model;
public enum OrderStatus {
PENDING, PAID, SHIPPED, DELIVERED, CANCELLED
}
Step 2.2: Create Interfaces (Dependencies)
Create src/main/java/com/example/shop/repository/OrderRepository.java:
package com.example.shop.repository;
import com.example.shop.model.Order;
import java.util.Optional;
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(Long id);
void delete(Long id);
}
Create src/main/java/com/example/shop/service/PaymentGateway.java:
package com.example.shop.service;
import java.math.BigDecimal;
public interface PaymentGateway {
boolean processPayment(Long customerId, BigDecimal amount);
void refund(Long customerId, BigDecimal amount);
}
Create src/main/java/com/example/shop/service/NotificationService.java:
package com.example.shop.service;
public interface NotificationService {
void sendOrderConfirmation(Long customerId, Long orderId);
void sendPaymentFailure(Long customerId, String reason);
}
Step 2.3: Create OrderService (Class Under Test)
Create src/main/java/com/example/shop/service/OrderService.java:
package com.example.shop.service;
import com.example.shop.model.*;
import com.example.shop.repository.OrderRepository;
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final NotificationService notificationService;
// (#1:Dependencies injected via constructor)
public OrderService(OrderRepository orderRepository,
PaymentGateway paymentGateway,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
this.notificationService = notificationService;
}
public Order placeOrder(Order order) {
// (#2:Save order first)
Order savedOrder = orderRepository.save(order);
// (#3:Process payment)
boolean paymentSuccess = paymentGateway.processPayment(
order.getCustomerId(),
order.getTotal()
);
if (paymentSuccess) {
savedOrder.setStatus(OrderStatus.PAID);
orderRepository.save(savedOrder);
notificationService.sendOrderConfirmation(
order.getCustomerId(),
savedOrder.getId()
);
} else {
savedOrder.setStatus(OrderStatus.CANCELLED);
orderRepository.save(savedOrder);
notificationService.sendPaymentFailure(
order.getCustomerId(),
"Payment declined"
);
}
return savedOrder;
}
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("Order not found"));
if (order.getStatus() == OrderStatus.PAID) {
paymentGateway.refund(order.getCustomerId(), order.getTotal());
}
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}
Part 3: Basic Mocking
Step 3.1: Create Your First Mock Test
Create src/test/java/com/example/shop/service/OrderServiceTest.java:
package com.example.shop.service;
import com.example.shop.model.*;
import com.example.shop.repository.OrderRepository;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) // (#1:Enable Mockito annotations)
class OrderServiceTest {
@Mock // (#2:Create a mock of this interface)
private OrderRepository orderRepository;
@Mock
private PaymentGateway paymentGateway;
@Mock
private NotificationService notificationService;
@InjectMocks // (#3:Inject mocks into this object)
private OrderService orderService;
private Order testOrder;
@BeforeEach
void setUp() {
OrderItem item = new OrderItem(1L, "Laptop", 1, new BigDecimal("999.99"));
testOrder = new Order(1L, 100L, List.of(item));
}
@Test
@DisplayName("Place order successfully when payment succeeds")
void placeOrder_PaymentSuccess() {
// (#4:Arrange - Define mock behavior)
when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
when(paymentGateway.processPayment(eq(100L), any(BigDecimal.class))).thenReturn(true);
// (#5:Act - Call the method under test)
Order result = orderService.placeOrder(testOrder);
// (#6:Assert - Verify the result)
assertEquals(OrderStatus.PAID, result.getStatus());
// (#7:Verify - Check mock interactions)
verify(orderRepository, times(2)).save(any(Order.class)); // Saved twice
verify(paymentGateway).processPayment(eq(100L), any(BigDecimal.class));
verify(notificationService).sendOrderConfirmation(100L, 1L);
verify(notificationService, never()).sendPaymentFailure(anyLong(), anyString());
}
@Test
@DisplayName("Cancel order when payment fails")
void placeOrder_PaymentFails() {
// Arrange
when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
when(paymentGateway.processPayment(anyLong(), any(BigDecimal.class))).thenReturn(false);
// Act
Order result = orderService.placeOrder(testOrder);
// Assert
assertEquals(OrderStatus.CANCELLED, result.getStatus());
// Verify correct notifications sent
verify(notificationService, never()).sendOrderConfirmation(anyLong(), anyLong());
verify(notificationService).sendPaymentFailure(eq(100L), eq("Payment declined"));
}
}
Step 3.2: Run the Tests
cd mockito-lab
mvn test
Expected output:
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Part 4: Common Stubbing Patterns
Step 4.1: Different Ways to Stub
Add these tests to understand stubbing options:
@Test
@DisplayName("Stubbing patterns demonstration")
void stubbingPatterns() {
// (#1:Return specific value)
when(paymentGateway.processPayment(anyLong(), any())).thenReturn(true);
// (#2:Return different values on consecutive calls)
when(paymentGateway.processPayment(100L, new BigDecimal("50.00")))
.thenReturn(true) // First call
.thenReturn(false) // Second call
.thenReturn(true); // Third and subsequent calls
// (#3:Throw an exception)
when(orderRepository.findById(999L))
.thenThrow(new RuntimeException("Database error"));
// (#4:Answer - dynamic response based on arguments)
when(orderRepository.save(any(Order.class))).thenAnswer(invocation -> {
Order order = invocation.getArgument(0);
// Return the same order (simulate save)
return order;
});
// (#5:Do nothing for void methods)
doNothing().when(notificationService).sendOrderConfirmation(anyLong(), anyLong());
// (#6:Throw exception for void methods)
doThrow(new RuntimeException("Email server down"))
.when(notificationService).sendPaymentFailure(anyLong(), anyString());
}
Step 4.2: Argument Matchers
@Test
@DisplayName("Argument matchers demonstration")
void argumentMatchers() {
// (#1:any() - match any value)
when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
// (#2:eq() - exact match (use with other matchers))
when(paymentGateway.processPayment(eq(100L), any(BigDecimal.class))).thenReturn(true);
// (#3:anyLong(), anyString(), etc.)
when(orderRepository.findById(anyLong())).thenReturn(java.util.Optional.of(testOrder));
// (#4:Custom matcher with argThat)
when(paymentGateway.processPayment(
anyLong(),
argThat(amount -> amount.compareTo(BigDecimal.ZERO) > 0))) // Amount must be positive
.thenReturn(true);
// (#5:isNull(), isNotNull()
when(orderRepository.findById(isNull())).thenReturn(java.util.Optional.empty());
}
If you use an argument matcher for one argument, you must use matchers for ALL arguments:
// WRONG - mixing matcher and raw value
when(mock.method(anyLong(), 100L)).thenReturn(result);
// CORRECT - use eq() for raw values
when(mock.method(anyLong(), eq(100L))).thenReturn(result);
Part 5: Verification Patterns
Step 5.1: Verify Method Calls
@Test
@DisplayName("Verification patterns demonstration")
void verificationPatterns() {
// Setup
when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
when(paymentGateway.processPayment(anyLong(), any())).thenReturn(true);
// Execute
orderService.placeOrder(testOrder);
// (#1:Verify method was called)
verify(paymentGateway).processPayment(anyLong(), any());
// (#2:Verify exact number of calls)
verify(orderRepository, times(2)).save(any(Order.class));
// (#3:Verify at least/at most)
verify(orderRepository, atLeast(1)).save(any());
verify(orderRepository, atMost(3)).save(any());
// (#4:Verify never called)
verify(notificationService, never()).sendPaymentFailure(anyLong(), anyString());
// (#5:Verify call order)
InOrder inOrder = inOrder(orderRepository, paymentGateway, notificationService);
inOrder.verify(orderRepository).save(any()); // First
inOrder.verify(paymentGateway).processPayment(anyLong(), any()); // Then
inOrder.verify(notificationService).sendOrderConfirmation(anyLong(), anyLong()); // Last
// (#6:Verify no more interactions)
verifyNoMoreInteractions(paymentGateway);
}
Part 6: ArgumentCaptor
Step 6.1: Capture and Inspect Arguments
@Test
@DisplayName("Use ArgumentCaptor to inspect method arguments")
void argumentCaptor() {
// Setup
when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
when(paymentGateway.processPayment(anyLong(), any())).thenReturn(true);
// Execute
orderService.placeOrder(testOrder);
// (#1:Create captor)
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class);
// (#2:Capture the argument)
verify(orderRepository, times(2)).save(orderCaptor.capture());
// (#3:Get all captured values)
List<Order> capturedOrders = orderCaptor.getAllValues();
assertEquals(2, capturedOrders.size());
// (#4:Inspect first call (initial save)
Order firstSave = capturedOrders.get(0);
assertEquals(OrderStatus.PENDING, firstSave.getStatus());
// (#5:Inspect second call (after payment)
Order secondSave = capturedOrders.get(1);
assertEquals(OrderStatus.PAID, secondSave.getStatus());
}
@Test
@DisplayName("Capture primitive arguments")
void captorForPrimitives() {
when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
when(paymentGateway.processPayment(anyLong(), any())).thenReturn(true);
orderService.placeOrder(testOrder);
// (#6:Capture BigDecimal argument)
ArgumentCaptor<BigDecimal> amountCaptor = ArgumentCaptor.forClass(BigDecimal.class);
verify(paymentGateway).processPayment(anyLong(), amountCaptor.capture());
BigDecimal capturedAmount = amountCaptor.getValue();
assertEquals(new BigDecimal("999.99"), capturedAmount);
}
- When you need to inspect complex objects passed to mocks
- When simple matchers aren't enough
- When you want to verify multiple calls with different arguments
Part 7: @Spy - Partial Mocking
Step 7.1: Understanding @Spy
Create src/main/java/com/example/shop/service/DiscountCalculator.java:
package com.example.shop.service;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class DiscountCalculator {
public BigDecimal calculateDiscount(BigDecimal total, int loyaltyPoints) {
BigDecimal baseDiscount = calculateBaseDiscount(total);
BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(loyaltyPoints);
return baseDiscount.add(loyaltyDiscount);
}
// (#1:We might want to mock this independently)
public BigDecimal calculateBaseDiscount(BigDecimal total) {
if (total.compareTo(new BigDecimal("100")) >= 0) {
return total.multiply(new BigDecimal("0.10")).setScale(2, RoundingMode.HALF_UP);
}
return BigDecimal.ZERO;
}
public BigDecimal calculateLoyaltyDiscount(int points) {
return new BigDecimal(points).multiply(new BigDecimal("0.01")).setScale(2, RoundingMode.HALF_UP);
}
}
Step 7.2: Using @Spy
Create src/test/java/com/example/shop/service/DiscountCalculatorTest.java:
package com.example.shop.service;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DiscountCalculatorTest {
@Spy // (#1:Real object with selective mocking)
private DiscountCalculator calculator;
@Test
@DisplayName("@Spy allows partial mocking")
void partialMocking() {
// (#2:Override one method, keep others real)
doReturn(new BigDecimal("20.00"))
.when(calculator).calculateBaseDiscount(any(BigDecimal.class));
// (#3:Call the main method)
BigDecimal result = calculator.calculateDiscount(
new BigDecimal("150.00"),
50 // 50 points = $0.50 loyalty discount
);
// (#4:Base discount is mocked (20.00), loyalty is real (0.50))
assertEquals(new BigDecimal("20.50"), result);
// (#5:Verify the mocked method was called)
verify(calculator).calculateBaseDiscount(new BigDecimal("150.00"));
}
@Test
@DisplayName("@Spy uses real methods by default")
void realMethodsByDefault() {
// No stubbing - uses real implementation
BigDecimal result = calculator.calculateDiscount(
new BigDecimal("200.00"), // 10% = $20
100 // 100 points = $1.00
);
assertEquals(new BigDecimal("21.00"), result);
}
}
| @Mock | @Spy |
|---|---|
| All methods return default (null, 0, false) | All methods use real implementation |
| Must stub every method you call | Only stub methods you want to override |
| Use for interfaces/dependencies | Use for partial mocking of concrete classes |
Part 8: BDD Style Testing
Step 8.1: Given-When-Then Pattern
package com.example.shop.service;
import com.example.shop.model.*;
import com.example.shop.repository.OrderRepository;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;
// (#1:Import BDD style methods)
import static org.mockito.BDDMockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceBDDTest {
@Mock private OrderRepository orderRepository;
@Mock private PaymentGateway paymentGateway;
@Mock private NotificationService notificationService;
@InjectMocks private OrderService orderService;
@Test
@DisplayName("Should send confirmation when payment succeeds")
void shouldSendConfirmationWhenPaymentSucceeds() {
// (#2:GIVEN - setup preconditions)
OrderItem item = new OrderItem(1L, "Book", 2, new BigDecimal("25.00"));
Order order = new Order(1L, 100L, List.of(item));
given(orderRepository.save(any(Order.class))).willReturn(order);
given(paymentGateway.processPayment(eq(100L), any())).willReturn(true);
// (#3:WHEN - execute the action)
Order result = orderService.placeOrder(order);
// (#4:THEN - verify outcomes)
then(orderRepository).should(times(2)).save(any(Order.class));
then(paymentGateway).should().processPayment(eq(100L), eq(new BigDecimal("50.00")));
then(notificationService).should().sendOrderConfirmation(100L, 1L);
then(notificationService).shouldHaveNoMoreInteractions();
assertEquals(OrderStatus.PAID, result.getStatus());
}
@Test
@DisplayName("Should send failure notification when payment fails")
void shouldSendFailureNotificationWhenPaymentFails() {
// GIVEN
OrderItem item = new OrderItem(1L, "Book", 1, new BigDecimal("10.00"));
Order order = new Order(2L, 200L, List.of(item));
given(orderRepository.save(any(Order.class))).willReturn(order);
given(paymentGateway.processPayment(anyLong(), any())).willReturn(false);
// WHEN
Order result = orderService.placeOrder(order);
// THEN
then(notificationService).should(never()).sendOrderConfirmation(anyLong(), anyLong());
then(notificationService).should().sendPaymentFailure(eq(200L), anyString());
assertEquals(OrderStatus.CANCELLED, result.getStatus());
}
}
given()is more readable thanwhen()for setupthen().should()reads like a specification- Tests read like documentation
- Follows the Arrange-Act-Assert pattern naturally
Deliverables Checklist
- OrderServiceTest runs with all tests passing
- Used @Mock and @InjectMocks correctly
- Used when().thenReturn() for stubbing
- Used verify() for interaction testing
- Used ArgumentCaptor to inspect arguments
- Used @Spy for partial mocking
- Converted at least one test to BDD style
Mockito Cheat Sheet
// Stubbing
when(mock.method()).thenReturn(value);
when(mock.method()).thenThrow(exception);
doNothing().when(mock).voidMethod();
doThrow(exception).when(mock).voidMethod();
// Verification
verify(mock).method();
verify(mock, times(2)).method();
verify(mock, never()).method();
verify(mock, atLeast(1)).method();
verifyNoMoreInteractions(mock);
// Matchers
any(), anyLong(), anyString()
eq(value)
argThat(arg -> condition)
isNull(), isNotNull()
// Captor
ArgumentCaptor<Type> captor = ArgumentCaptor.forClass(Type.class);
verify(mock).method(captor.capture());
Type captured = captor.getValue();
List<Type> allValues = captor.getAllValues();
// BDD
given(mock.method()).willReturn(value);
then(mock).should().method();
then(mock).should(never()).method();
Bonus Challenges
Use verify(mock, timeout(1000)).method() to verify a method was called within a time limit.
Use mockStatic() to mock static methods like LocalDateTime.now().
Use @MockBean in a Spring Boot test to replace beans with mocks.