ArgumentCaptor란?
ArgumentCaptor는 Mockito 라이브러리에서 제공하는 클래스로, Mock 객체의 메서드 호출 시 전달된 인자를 캡처하여 나중에 검증할 수 있게 해주는 도구입니다.
일반적인 verify() 검증에서는 eq(), any() 등의 matcher를 사용하여 인자를 검증하지만, ArgumentCaptor를 사용하면 실제로 전달된 인자 객체를 직접 가져와서 더 세밀한 검증이 가능합니다.
언제 사용하면 좋은가?
ArgumentCaptor는 다음과 같은 상황에서 특히 유용합니다:
- 복잡한 객체 검증: 메서드에 전달된 객체의 여러 필드를 상세히 검증하고 싶을 때
- 동적으로 생성된 인자 검증: 테스트 대상 코드 내부에서 생성된 객체를 검증할 때
- 다중 호출 검증: 같은 메서드가 여러 번 호출될 때 각 호출의 인자를 개별적으로 검증하고 싶을 때
- 람다/콜백 검증: 전달된 람다나 콜백 함수를 검증할 때
기본 사용법
ArgumentCaptor 생성
// 방법 1: forClass() 정적 메서드 사용
ArgumentCaptor<String> stringCaptor = ArgumentCaptor.forClass(String.class);
// 방법 2: captor() 정적 메서드 사용 (Mockito 4.0+)
ArgumentCaptor<User> userCaptor = ArgumentCaptor.captor();
핵심 메서드
| 메서드 | 설명 |
|---|---|
capture() |
verify() 내에서 인자를 캡처할 위치 지정 |
getValue() |
마지막으로 캡처된 값 반환 |
getAllValues() |
캡처된 모든 값을 List로 반환 |
기본 예제
단일 인자 캡처
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void 사용자_저장시_전달된_객체를_검증한다() {
// given
String name = "홍길동";
String email = "hong@example.com";
// when
userService.createUser(name, email);
// then
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertThat(capturedUser.getName()).isEqualTo("홍길동");
assertThat(capturedUser.getEmail()).isEqualTo("hong@example.com");
assertThat(capturedUser.getCreatedAt()).isNotNull();
}
}
위 예제에서 userService.createUser() 내부에서 생성된 User 객체를 캡처하여 각 필드를 검증하고 있습니다.
@Captor 어노테이션
필드 레벨에서 @Captor 어노테이션을 사용하면 더 간결하게 ArgumentCaptor를 선언할 수 있습니다.
@ExtendWith(MockitoExtension.class)
class EmailServiceTest {
@Mock
private EmailSender emailSender;
@InjectMocks
private NotificationService notificationService;
@Captor
private ArgumentCaptor<EmailMessage> emailCaptor;
@Test
void 알림_발송시_이메일_내용을_검증한다() {
// given
String userId = "user123";
// when
notificationService.sendWelcomeEmail(userId);
// then
verify(emailSender).send(emailCaptor.capture());
EmailMessage captured = emailCaptor.getValue();
assertThat(captured.getSubject()).contains("환영");
assertThat(captured.getBody()).contains(userId);
}
}
@Captor 사용의 장점
- 제네릭 타입을 명시적으로 선언하여 타입 안전성 확보
- 여러 테스트 메서드에서 재사용 가능
- 코드가 더 간결해짐
다중 호출 캡처
같은 메서드가 여러 번 호출될 때 getAllValues()를 사용하여 모든 호출의 인자를 검증할 수 있습니다.
@Test
void 배치_처리시_각_항목별_로그를_검증한다() {
// given
List<Order> orders = List.of(
new Order("ORD-001", 10000),
new Order("ORD-002", 20000),
new Order("ORD-003", 30000)
);
// when
orderService.processBatch(orders);
// then
ArgumentCaptor<String> logCaptor = ArgumentCaptor.forClass(String.class);
verify(logger, times(3)).info(logCaptor.capture());
List<String> allLogs = logCaptor.getAllValues();
assertThat(allLogs).hasSize(3);
assertThat(allLogs.get(0)).contains("ORD-001");
assertThat(allLogs.get(1)).contains("ORD-002");
assertThat(allLogs.get(2)).contains("ORD-003");
}
복잡한 객체 검증
ArgumentCaptor는 복잡한 중첩 객체나 컬렉션을 포함한 인자를 검증할 때 특히 강력합니다.
@Test
void 주문_생성시_전달된_주문_상세를_검증한다() {
// given
List<CartItem> cartItems = List.of(
new CartItem("PROD-001", 2, 5000),
new CartItem("PROD-002", 1, 15000)
);
// when
orderService.createOrder("USER-001", cartItems);
// then
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class);
verify(orderRepository).save(orderCaptor.capture());
Order captured = orderCaptor.getValue();
// 기본 정보 검증
assertThat(captured.getUserId()).isEqualTo("USER-001");
assertThat(captured.getStatus()).isEqualTo(OrderStatus.PENDING);
// 주문 항목 검증
assertThat(captured.getItems()).hasSize(2);
assertThat(captured.getTotalAmount()).isEqualTo(25000);
// 첫 번째 항목 상세 검증
OrderItem firstItem = captured.getItems().get(0);
assertThat(firstItem.getProductId()).isEqualTo("PROD-001");
assertThat(firstItem.getQuantity()).isEqualTo(2);
}
ArgumentCaptor vs ArgumentMatcher
| 구분 | ArgumentCaptor | ArgumentMatcher (eq, any 등) |
|---|---|---|
| 용도 | 인자를 캡처하여 나중에 검증 | 호출 시점에 인자 조건 검증 |
| 유연성 | 캡처 후 다양한 assertion 가능 | 단순 조건 검증에 적합 |
| 가독성 | 검증 로직이 분리됨 | 한 줄로 간결하게 표현 |
| 권장 사용 | 복잡한 객체 검증 | 단순 값 검증 |
비교 예제
// ArgumentMatcher 사용 - 단순한 경우
verify(userRepository).save(argThat(user ->
user.getName().equals("홍길동")
));
// ArgumentCaptor 사용 - 복잡한 검증이 필요한 경우
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(captor.capture());
User captured = captor.getValue();
assertThat(captured.getName()).isEqualTo("홍길동");
assertThat(captured.getEmail()).matches(".*@example\\.com");
assertThat(captured.getRoles()).containsExactly(Role.USER);
assertThat(captured.getCreatedAt()).isAfter(LocalDateTime.now().minusMinutes(1));
주의사항
1. Stubbing보다는 Verification에서 사용
ArgumentCaptor는 stubbing(when())보다 verification(verify())에서 사용하는 것이 권장됩니다.
// 권장하지 않음 - stubbing에서 사용
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
when(mockService.process(captor.capture())).thenReturn("result");
// 권장 - verification에서 사용
verify(mockService).process(captor.capture());
stubbing에서 capture()를 사용하면 실제 테스트 실행 전에 캡처가 발생하여 예상치 못한 동작이 발생할 수 있습니다.
2. getValue()는 마지막 값만 반환
여러 번 호출된 경우 getValue()는 마지막 캡처된 값만 반환합니다. 모든 값이 필요하면 getAllValues()를 사용하세요.
// 3번 호출된 경우
verify(service, times(3)).process(captor.capture());
// getValue()는 마지막(3번째) 호출의 인자만 반환
String lastValue = captor.getValue();
// getAllValues()는 모든 호출의 인자를 순서대로 반환
List<String> allValues = captor.getAllValues();
3. 제네릭 타입과 타입 안전성
제네릭 컬렉션을 캡처할 때는 @Captor 어노테이션을 사용하면 타입 안전성을 확보할 수 있습니다.
// 컴파일 경고 발생 가능
ArgumentCaptor<List<String>> captor = ArgumentCaptor.forClass(List.class);
// @Captor 사용으로 타입 안전성 확보
@Captor
private ArgumentCaptor<List<String>> listCaptor;
실전 활용 패턴
이벤트 발행 검증
@Test
void 주문_완료시_이벤트가_발행된다() {
// given
Order order = new Order("ORD-001", 50000);
// when
orderService.complete(order);
// then
ArgumentCaptor<OrderCompletedEvent> eventCaptor =
ArgumentCaptor.forClass(OrderCompletedEvent.class);
verify(eventPublisher).publish(eventCaptor.capture());
OrderCompletedEvent event = eventCaptor.getValue();
assertThat(event.getOrderId()).isEqualTo("ORD-001");
assertThat(event.getAmount()).isEqualTo(50000);
assertThat(event.getOccurredAt()).isNotNull();
}
외부 API 호출 검증
@Test
void 결제_요청시_올바른_파라미터로_API를_호출한다() {
// given
PaymentRequest request = new PaymentRequest("USER-001", 10000);
// when
paymentService.pay(request);
// then
ArgumentCaptor<HttpEntity<PaymentApiRequest>> captor =
ArgumentCaptor.forClass(HttpEntity.class);
verify(restTemplate).postForEntity(
eq("https://api.payment.com/pay"),
captor.capture(),
eq(PaymentApiResponse.class)
);
PaymentApiRequest apiRequest = captor.getValue().getBody();
assertThat(apiRequest.getMerchantId()).isEqualTo("MY_MERCHANT");
assertThat(apiRequest.getAmount()).isEqualTo(10000);
assertThat(apiRequest.getSignature()).isNotBlank();
}
정리
ArgumentCaptor는 Mockito를 사용한 단위 테스트에서 메서드에 전달된 인자를 상세하게 검증할 때 매우 유용한 도구입니다.
핵심 포인트:
capture()로 인자를 캡처하고getValue()또는getAllValues()로 조회@Captor어노테이션으로 간결하게 선언 가능- 복잡한 객체나 동적으로 생성된 인자 검증에 적합
- stubbing보다는 verification에서 사용하는 것이 권장됨