프로그래밍/트러블슈팅
📌 [Java] Spring WebFlux Mockito 유닛 테스트에서 verify(valueOperations).set() 호출되지 않는 문제 해결
프로그래민구찌
2025. 2. 26. 15:13
Spring WebFlux 애플리케이션에서 Mockito를 사용하여 유닛 테스트를 작성할 때, 특정 메서드가 예상대로 호출되지 않는 문제가 발생할 수 있습니다. 이번 글에서는 Mockito를 활용한 verify(valueOperations).set() 검증이 실패하는 문제와 이를 해결하는 방법을 알아보겠습니다.
⚠️ 문제 상황
Spring Boot + WebFlux 환경에서 사용자 정보를 업데이트한 후 Redis에 캐시하는 기능을 테스트하는 도중, 다음과 같은 오류가 발생했습니다.
에러 메시지
Wanted but not invoked:
valueOperations.set(
"user:1",
User(id=1, name=Updated John, email=updated@example.com)
);
Actually, there were zero interactions with this mock.
해당 메시지는 verify(valueOperations).set("user:1", updatedUser);이 한 번도 호출되지 않았음을 의미합니다.
즉, updateUser() 메서드 내에서 Redis 저장 로직이 실행되지 않은 것으로 보입니다.
💡 원인 분석
1️⃣ Mockito의 객체 매칭 문제
테스트 코드에서 다음과 같이 when(...).thenReturn(...)을 사용하고 있었습니다.
when(valueOperations.set("user:1", updatedUser)).thenReturn(Mono.just(true));
⚠️ 문제점:
- Mockito는 객체가 완전히 동일해야 thenReturn()을 실행합니다.
- 하지만 updatedUser 객체는 테스트 실행 중 내부적으로 변경될 가능성이 있어 Mockito가 동일한 객체로 인식하지 못할 수 있습니다.
2️⃣ verify() 검증 시 객체가 다르게 인식됨
verify(valueOperations).set("user:1", updatedUser);
⚠️ 문제점:
- 테스트 실행 중 updatedUser 객체가 내부적으로 변경될 가능성이 있습니다.
- Mockito는 객체의 참조값을 비교하기 때문에, 동일한 필드 값을 가지더라도 새로운 인스턴스가 생성되면 다르게 인식할 수 있습니다.
3️⃣ Reactive 체인이 실행되지 않음
- Mono는 구독(subscribe())되지 않으면 실행되지 않습니다.
- StepVerifier를 사용하지 않거나 잘못된 방식으로 검증하면, Reactive 체인이 실행되지 않아 내부 코드가 동작하지 않음.
🚀 해결 방법
✅ 1. Mockito의 객체 매칭 문제 해결
기존 코드 (객체 매칭 실패 가능성 존재)
when(valueOperations.set("user:1", updatedUser)).thenReturn(Mono.just(true));
수정 코드 (any(User.class) 사용)
when(valueOperations.set(eq("user:1"), any(User.class))).thenReturn(Mono.just(true));
✔ eq("user:1") → "user:1" 키 값은 정확히 일치하도록 유지
✔ any(User.class) → updatedUser 객체가 내부적으로 변하더라도 허용
✅ 2. verify() 검증 방식 수정
기존 코드 (객체 매칭 실패 가능성)
verify(valueOperations).set("user:1", updatedUser);
수정 코드 (any(User.class) 사용)
verify(valueOperations).set(eq("user:1"), any(User.class));
✅ 3. Mono 체인이 실행되도록 StepVerifier 적용
기존 코드 (Mono가 실행되지 않을 가능성)
StepVerifier.create(updatedUserMono).verifyComplete();
수정 코드 (Mono 실행 여부 확인)
StepVerifier.create(updatedUserMono)
.expectNextMatches(user -> {
System.out.println("🔍 StepVerifier - 업데이트된 사용자: " + user);
assertEquals("Updated John", user.getName());
return true;
})
.verifyComplete();
✔ System.out.println()을 활용하여 체인 실행 여부를 직접 확인
✔ StepVerifier.create()가 Mono를 강제로 실행하도록 설정
🛠 최종 수정된 테스트 코드
@Test
void updateUser_ShouldUpdateInDB_AndCacheInRedis() {
// Given: 기존 사용자 객체 생성
User updatedUser = new User();
updatedUser.setId(1L);
updatedUser.setName("Updated John");
updatedUser.setEmail("updated@example.com");
when(userRepository.save(any(User.class))).thenReturn(Mono.just(updatedUser));
when(valueOperations.set(eq("user:1"), any(User.class))).thenReturn(Mono.just(true));
// When: 사용자 업데이트 메서드 호출
Mono<User> updatedUserMono = userService.updateUser(1L, updatedUser);
// Then: 반환된 값 검증 및 로그 확인
StepVerifier.create(updatedUserMono)
.expectNextMatches(user -> {
System.out.println("🔍 StepVerifier - 업데이트된 사용자: " + user);
assertEquals("Updated John", user.getName());
return true;
})
.verifyComplete();
// 데이터베이스에서 사용자 정보가 업데이트되었는지 확인
verify(userRepository).save(any(User.class));
// Redis에 사용자 정보가 업데이트되었는지 확인
verify(valueOperations).set(eq("user:1"), any(User.class));
}
🎯 테스트 성공 로그
테스트 실행 후, 다음과 같은 로그가 정상적으로 출력되면 문제 해결 완료입니다! ✅
🔍 StepVerifier - 업데이트된 사용자: User(id=1, name=Updated John, email=updated@example.com)
💡 결론: Mockito 객체 매칭 문제를 주의하자!
이번 문제를 해결하면서 얻은 교훈:
- Mockito는 객체의 "참조값"을 비교하므로, any(User.class)를 사용하여 유연성을 확보하는 것이 중요하다.
- Reactive Streams (Mono, Flux)는 반드시 StepVerifier나 subscribe()를 사용하여 실행을 보장해야 한다.
- verify() 검증 시, eq("key") + any(Class) 조합을 사용하면 불필요한 매칭 실패를 방지할 수 있다.
참고 자료