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.