Overview
ArgumentCaptor is a Mockito feature that allows you to capture argument values passed to mocked methods for further verification and analysis. It's particularly useful when you need to verify complex objects or perform multiple assertions on method arguments.
Basic Usage
1. Simple Argument Captor
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
public class BasicArgumentCaptorTest {
@Mock
private List<String> mockedList;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testBasicArgumentCaptor() {
// Create ArgumentCaptor for String
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
// Perform some operations on the mock
mockedList.add("hello");
mockedList.add("world");
// Verify and capture arguments
verify(mockedList, times(2)).add(argumentCaptor.capture());
// Get all captured values
List<String> allValues = argumentCaptor.getAllValues();
assertEquals(2, allValues.size());
assertEquals("hello", allValues.get(0));
assertEquals("world", allValues.get(1));
}
@Test
void testSingleValueCapture() {
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
mockedList.add("single value");
verify(mockedList).add(captor.capture());
String capturedValue = captor.getValue();
assertEquals("single value", capturedValue);
}
}
2. Capturing Multiple Arguments
import java.util.Map;
public class MultipleArgumentsTest {
@Mock
private Map<String, Integer> mockedMap;
@Test
void testCapturingMultipleMethodCalls() {
ArgumentCaptor<String> keyCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<Integer> valueCaptor = ArgumentCaptor.forClass(Integer.class);
// Perform operations
mockedMap.put("key1", 100);
mockedMap.put("key2", 200);
mockedMap.put("key3", 300);
// Verify and capture
verify(mockedMap, times(3)).put(keyCaptor.capture(), valueCaptor.capture());
// Get all captured values
List<String> keys = keyCaptor.getAllValues();
List<Integer> values = valueCaptor.getAllValues();
assertEquals(3, keys.size());
assertEquals(3, values.size());
assertEquals("key1", keys.get(0));
assertEquals(100, values.get(0));
assertEquals("key3", keys.get(2));
assertEquals(300, values.get(2));
}
}
Advanced Usage
1. Capturing Complex Objects
public class ComplexObjectCaptureTest {
// Complex domain objects
public static class User {
private String username;
private String email;
private int age;
public User(String username, String email, int age) {
this.username = username;
this.email = email;
this.age = age;
}
// Getters and setters
public String getUsername() { return username; }
public String getEmail() { return email; }
public int getAge() { return age; }
}
public static class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void createUser(String username, String email, int age) {
User user = new User(username, email, age);
userRepository.save(user);
}
public void updateUserEmail(String username, String newEmail) {
User user = userRepository.findByUsername(username);
if (user != null) {
User updatedUser = new User(user.getUsername(), newEmail, user.getAge());
userRepository.save(updatedUser);
}
}
}
public interface UserRepository {
void save(User user);
User findByUsername(String username);
}
@Mock
private UserRepository userRepository;
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
userService = new UserService(userRepository);
}
@Test
void testCapturingComplexObject() {
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
userService.createUser("john_doe", "[email protected]", 30);
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertEquals("john_doe", capturedUser.getUsername());
assertEquals("[email protected]", capturedUser.getEmail());
assertEquals(30, capturedUser.getAge());
}
@Test
void testCapturingMultipleComplexObjects() {
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
// Setup mock behavior
when(userRepository.findByUsername("jane_doe"))
.thenReturn(new User("jane_doe", "[email protected]", 25));
userService.updateUserEmail("jane_doe", "[email protected]");
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertEquals("jane_doe", capturedUser.getUsername());
assertEquals("[email protected]", capturedUser.getEmail());
assertEquals(25, capturedUser.getAge());
}
}
2. Capturing with Custom Matchers
public class CaptorWithMatchersTest {
@Mock
private NotificationService notificationService;
@Test
void testCaptorWithAdditionalVerification() {
ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> recipientCaptor = ArgumentCaptor.forClass(String.class);
notificationService.sendNotification("Hello, User!", "[email protected]");
notificationService.sendNotification("Important update!", "[email protected]");
// Verify with captor and additional conditions
verify(notificationService, times(2))
.sendNotification(messageCaptor.capture(), recipientCaptor.capture());
List<String> messages = messageCaptor.getAllValues();
List<String> recipients = recipientCaptor.getAllValues();
// Additional assertions
assertTrue(messages.get(0).contains("Hello"));
assertTrue(recipients.get(1).contains("admin"));
// Verify specific conditions
assertEquals("[email protected]", recipients.get(0));
assertEquals("Important update!", messages.get(1));
}
}
interface NotificationService {
void sendNotification(String message, String recipient);
}
Practical Examples
Example 1: Email Service Testing
public class EmailServiceTest {
public static class Email {
private final String to;
private final String subject;
private final String body;
private final List<String> cc;
public Email(String to, String subject, String body, List<String> cc) {
this.to = to;
this.subject = subject;
this.body = body;
this.cc = cc != null ? new ArrayList<>(cc) : new ArrayList<>();
}
// Getters
public String getTo() { return to; }
public String getSubject() { return subject; }
public String getBody() { return body; }
public List<String> getCc() { return Collections.unmodifiableList(cc); }
}
public interface EmailSender {
void sendEmail(Email email);
void sendBulkEmails(List<Email> emails);
}
public static class EmailService {
private final EmailSender emailSender;
private final List<Email> sentEmails = new ArrayList<>();
public EmailService(EmailSender emailSender) {
this.emailSender = emailSender;
}
public void sendWelcomeEmail(String to, String username) {
String subject = "Welcome to Our Service!";
String body = String.format("Hello %s, welcome to our service!", username);
Email email = new Email(to, subject, body, null);
emailSender.sendEmail(email);
sentEmails.add(email);
}
public void sendBulkNotification(List<String> recipients, String message) {
List<Email> emails = recipients.stream()
.map(recipient -> new Email(recipient, "Notification", message, null))
.collect(Collectors.toList());
emailSender.sendBulkEmails(emails);
sentEmails.addAll(emails);
}
}
@Mock
private EmailSender emailSender;
private EmailService emailService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
emailService = new EmailService(emailSender);
}
@Test
void testWelcomeEmailContent() {
ArgumentCaptor<Email> emailCaptor = ArgumentCaptor.forClass(Email.class);
emailService.sendWelcomeEmail("[email protected]", "John Doe");
verify(emailSender).sendEmail(emailCaptor.capture());
Email capturedEmail = emailCaptor.getValue();
assertEquals("[email protected]", capturedEmail.getTo());
assertEquals("Welcome to Our Service!", capturedEmail.getSubject());
assertTrue(capturedEmail.getBody().contains("John Doe"));
assertTrue(capturedEmail.getBody().contains("welcome to our service"));
}
@Test
void testBulkEmailSending() {
ArgumentCaptor<List<Email>> emailsCaptor = ArgumentCaptor.forClass(List.class);
List<String> recipients = Arrays.asList(
"[email protected]",
"[email protected]",
"[email protected]"
);
String message = "System maintenance scheduled";
emailService.sendBulkNotification(recipients, message);
verify(emailSender).sendBulkEmails(emailsCaptor.capture());
List<Email> capturedEmails = emailsCaptor.getValue();
assertEquals(3, capturedEmails.size());
// Verify each email
for (int i = 0; i < recipients.size(); i++) {
Email email = capturedEmails.get(i);
assertEquals(recipients.get(i), email.getTo());
assertEquals("Notification", email.getSubject());
assertEquals(message, email.getBody());
}
}
}
Example 2: Order Processing System
public class OrderProcessingTest {
public static class Order {
private String orderId;
private String customerId;
private List<OrderItem> items;
private double totalAmount;
private OrderStatus status;
public Order(String orderId, String customerId, List<OrderItem> items) {
this.orderId = orderId;
this.customerId = customerId;
this.items = new ArrayList<>(items);
this.totalAmount = calculateTotalAmount();
this.status = OrderStatus.PENDING;
}
private double calculateTotalAmount() {
return items.stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
}
// Getters and setters
public String getOrderId() { return orderId; }
public String getCustomerId() { return customerId; }
public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
public double getTotalAmount() { return totalAmount; }
public OrderStatus getStatus() { return status; }
public void setStatus(OrderStatus status) { this.status = status; }
}
public static class OrderItem {
private String productId;
private String productName;
private int quantity;
private double price;
public OrderItem(String productId, String productName, int quantity, double price) {
this.productId = productId;
this.productName = productName;
this.quantity = quantity;
this.price = price;
}
// Getters
public String getProductId() { return productId; }
public String getProductName() { return productName; }
public int getQuantity() { return quantity; }
public double getPrice() { return price; }
}
public enum OrderStatus {
PENDING, PROCESSING, COMPLETED, CANCELLED
}
public interface OrderRepository {
void save(Order order);
Order findById(String orderId);
void updateStatus(String orderId, OrderStatus status);
}
public interface PaymentService {
boolean processPayment(String orderId, double amount, String customerId);
}
public interface InventoryService {
boolean reserveItems(String orderId, List<OrderItem> items);
void releaseItems(String orderId);
}
public static class OrderProcessor {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final InventoryService inventoryService;
public OrderProcessor(OrderRepository orderRepository,
PaymentService paymentService,
InventoryService inventoryService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
public boolean processOrder(Order order) {
try {
// Save initial order
orderRepository.save(order);
// Process payment
boolean paymentSuccess = paymentService.processPayment(
order.getOrderId(),
order.getTotalAmount(),
order.getCustomerId()
);
if (!paymentSuccess) {
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
return false;
}
// Reserve inventory
boolean inventoryReserved = inventoryService.reserveItems(
order.getOrderId(),
order.getItems()
);
if (!inventoryReserved) {
// Refund payment if inventory not available
// (simplified - in real scenario, you'd call payment service to refund)
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
return false;
}
// Mark order as completed
order.setStatus(OrderStatus.COMPLETED);
orderRepository.save(order);
return true;
} catch (Exception e) {
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
return false;
}
}
}
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentService paymentService;
@Mock
private InventoryService inventoryService;
private OrderProcessor orderProcessor;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
orderProcessor = new OrderProcessor(orderRepository, paymentService, inventoryService);
}
@Test
void testSuccessfulOrderProcessing() {
// Setup test data
List<OrderItem> items = Arrays.asList(
new OrderItem("prod1", "Product 1", 2, 25.0),
new OrderItem("prod2", "Product 2", 1, 50.0)
);
Order order = new Order("order123", "customer456", items);
// Setup mock behavior
when(paymentService.processPayment(anyString(), anyDouble(), anyString()))
.thenReturn(true);
when(inventoryService.reserveItems(anyString(), anyList()))
.thenReturn(true);
// Execute
boolean result = orderProcessor.processOrder(order);
// Verify
assertTrue(result);
// Capture and verify order saves
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class);
verify(orderRepository, times(3)).save(orderCaptor.capture());
List<Order> savedOrders = orderCaptor.getAllValues();
// First save - initial order
assertEquals(OrderStatus.PENDING, savedOrders.get(0).getStatus());
// Last save - completed order
assertEquals(OrderStatus.COMPLETED, savedOrders.get(2).getStatus());
assertEquals("order123", savedOrders.get(2).getOrderId());
}
@Test
void testOrderProcessingWithPaymentFailure() {
List<OrderItem> items = Arrays.asList(
new OrderItem("prod1", "Product 1", 1, 100.0)
);
Order order = new Order("order456", "customer789", items);
// Payment fails
when(paymentService.processPayment(anyString(), anyDouble(), anyString()))
.thenReturn(false);
boolean result = orderProcessor.processOrder(order);
assertFalse(result);
// Capture order saves and verify cancellation
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class);
verify(orderRepository, times(2)).save(orderCaptor.capture());
List<Order> savedOrders = orderCaptor.getAllValues();
assertEquals(OrderStatus.CANCELLED, savedOrders.get(1).getStatus());
// Verify inventory service was NOT called
verify(inventoryService, never()).reserveItems(anyString(), anyList());
}
@Test
void testInventoryReservationParameters() {
List<OrderItem> items = Arrays.asList(
new OrderItem("prod1", "Product 1", 2, 25.0),
new OrderItem("prod2", "Product 2", 1, 50.0)
);
Order order = new Order("order789", "customer123", items);
when(paymentService.processPayment(anyString(), anyDouble(), anyString()))
.thenReturn(true);
when(inventoryService.reserveItems(anyString(), anyList()))
.thenReturn(true);
orderProcessor.processOrder(order);
// Capture inventory reservation call
ArgumentCaptor<String> orderIdCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<List<OrderItem>> itemsCaptor = ArgumentCaptor.forClass(List.class);
verify(inventoryService).reserveItems(orderIdCaptor.capture(), itemsCaptor.capture());
assertEquals("order789", orderIdCaptor.getValue());
List<OrderItem> capturedItems = itemsCaptor.getValue();
assertEquals(2, capturedItems.size());
// Verify items are correctly passed
OrderItem firstItem = capturedItems.get(0);
assertEquals("prod1", firstItem.getProductId());
assertEquals(2, firstItem.getQuantity());
assertEquals(25.0, firstItem.getPrice(), 0.001);
}
}
Advanced Techniques
1. Captoring with Verification Mode
public class AdvancedCaptorTechniques {
@Mock
private DataService dataService;
@Test
void testCaptorWithVerificationMode() {
ArgumentCaptor<String> queryCaptor = ArgumentCaptor.forClass(String.class);
dataService.executeQuery("SELECT * FROM users");
dataService.executeQuery("SELECT * FROM orders");
dataService.executeQuery("SELECT * FROM products");
// Verify with specific verification mode
verify(dataService, atLeast(2)).executeQuery(queryCaptor.capture());
verify(dataService, atMost(5)).executeQuery(queryCaptor.capture());
List<String> allQueries = queryCaptor.getAllValues();
assertTrue(allQueries.size() >= 2);
}
@Test
void testCaptorWithTimeout() {
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
// Simulate async operation
new Thread(() -> {
try {
Thread.sleep(100);
dataService.executeQuery("async query");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// Verify with timeout
verify(dataService, timeout(500)).executeQuery(captor.capture());
assertEquals("async query", captor.getValue());
}
}
interface DataService {
void executeQuery(String query);
}
2. Generic Argument Captor
public class GenericCaptorTest {
public static class Response<T> {
private T data;
private boolean success;
public Response(T data, boolean success) {
this.data = data;
this.success = success;
}
public T getData() { return data; }
public boolean isSuccess() { return success; }
}
public interface ApiClient {
<T> Response<T> post(String endpoint, T data);
<T> Response<T> get(String endpoint, Class<T> responseType);
}
@Mock
private ApiClient apiClient;
@Test
void testGenericArgumentCaptor() {
// For generic types, use ArgumentCaptor with wildcard
@SuppressWarnings("unchecked")
ArgumentCaptor<Object> dataCaptor = ArgumentCaptor.forClass(Object.class);
ArgumentCaptor<String> endpointCaptor = ArgumentCaptor.forClass(String.class);
User user = new User("testuser", "[email protected]", 25);
apiClient.post("/users", user);
verify(apiClient).post(endpointCaptor.capture(), dataCaptor.capture());
assertEquals("/users", endpointCaptor.getValue());
User capturedUser = (User) dataCaptor.getValue();
assertEquals("testuser", capturedUser.getUsername());
assertEquals("[email protected]", capturedUser.getEmail());
}
@Test
void testSpecificGenericCaptor() {
// For specific generic types, use @Captor annotation with wildcard
@Captor
private ArgumentCaptor<User> userCaptor;
User user = new User("specific", "[email protected]", 30);
apiClient.post("/users", user);
verify(apiClient).post(anyString(), userCaptor.capture());
User captured = userCaptor.getValue();
assertEquals("specific", captured.getUsername());
}
}
3. Captoring in Ordered Verification
public class OrderedVerificationTest {
@Mock
private AuditService auditService;
@Test
void testCaptorWithInOrder() {
ArgumentCaptor<String> actionCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> userCaptor = ArgumentCaptor.forClass(String.class);
auditService.logAction("LOGIN", "user1");
auditService.logAction("VIEW_PAGE", "user1");
auditService.logAction("LOGOUT", "user1");
// Create in-order verification
InOrder inOrder = inOrder(auditService);
inOrder.verify(auditService).logAction(actionCaptor.capture(), userCaptor.capture());
inOrder.verify(auditService).logAction(actionCaptor.capture(), userCaptor.capture());
inOrder.verify(auditService).logAction(actionCaptor.capture(), userCaptor.capture());
List<String> actions = actionCaptor.getAllValues();
List<String> users = userCaptor.getAllValues();
assertEquals("LOGIN", actions.get(0));
assertEquals("VIEW_PAGE", actions.get(1));
assertEquals("LOGOUT", actions.get(2));
assertEquals("user1", users.get(0));
}
}
interface AuditService {
void logAction(String action, String username);
}
Best Practices
1. Proper Captor Usage
public class ArgumentCaptorBestPractices {
@Mock
private Service service;
@Captor
private ArgumentCaptor<String> stringCaptor;
@Captor
private ArgumentCaptor<Integer> integerCaptor;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void goodPractice_useCaptorAnnotation() {
// Good: Use @Captor annotation for cleaner code
service.process("data", 42);
verify(service).process(stringCaptor.capture(), integerCaptor.capture());
assertEquals("data", stringCaptor.getValue());
assertEquals(42, integerCaptor.getValue());
}
@Test
void goodPractice_captureAtVerificationTime() {
// Good: Capture at the time of verification
service.process("value1", 1);
service.process("value2", 2);
verify(service, times(2)).process(stringCaptor.capture(), integerCaptor.capture());
List<String> strings = stringCaptor.getAllValues();
List<Integer> integers = integerCaptor.getAllValues();
assertEquals(2, strings.size());
assertEquals(2, integers.size());
}
@Test
void badPractice_dontOveruseCaptors() {
// Bad: Don't use captors when simple verification suffices
service.process("simple", 123);
// Instead of using captor for simple equality check:
// verify(service).process(stringCaptor.capture(), integerCaptor.capture());
// assertEquals("simple", stringCaptor.getValue());
// Better: Use direct argument matchers
verify(service).process(eq("simple"), eq(123));
}
@Test
void goodPractice_combineWithOtherMatchers() {
// Good: Combine captors with other matchers when needed
service.process("important", 999);
service.process("other", 111);
// Capture only calls with specific first argument
verify(service).process(eq("important"), integerCaptor.capture());
assertEquals(999, integerCaptor.getValue());
}
}
interface Service {
void process(String data, int value);
}
Common Pitfalls and Solutions
public class CommonPitfalls {
@Mock
private Repository repository;
@Test
void pitfall_captorReuse() {
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
repository.save("first");
verify(repository).save(captor.capture());
assertEquals("first", captor.getValue());
// Pitfall: Reusing the same captor
repository.save("second");
verify(repository, times(2)).save(captor.capture());
// Solution: Be aware that getAllValues contains all captured values
List<String> allValues = captor.getAllValues();
assertEquals(2, allValues.size());
assertEquals("first", allValues.get(0));
assertEquals("second", allValues.get(1));
}
@Test
void pitfall_captorNotUsedInVerification() {
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
repository.save("data");
// Pitfall: Captor created but not used in verification
// verify(repository).save("data"); // This doesn't use the captor
// Solution: Use captor in verification
verify(repository).save(captor.capture());
assertEquals("data", captor.getValue());
}
@Test
void solution_resetCaptorBetweenTests() {
// Solution: Create new captor for each test or use @Captor annotation
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
repository.save("test");
verify(repository).save(captor.capture());
// Captor is local to test, so no need to reset
}
}
interface Repository {
void save(String data);
}
ArgumentCaptor is a powerful tool for testing complex interactions with mocks. Use it when you need to verify the state of arguments passed to mocked methods, especially when dealing with complex objects or when you need to perform multiple assertions on the same arguments.