프로그래밍/트러블슈팅

📌 [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 객체 매칭 문제를 주의하자!

이번 문제를 해결하면서 얻은 교훈:

  1. Mockito는 객체의 "참조값"을 비교하므로, any(User.class)를 사용하여 유연성을 확보하는 것이 중요하다.
  2. Reactive Streams (Mono, Flux)는 반드시 StepVerifier나 subscribe()를 사용하여 실행을 보장해야 한다.
  3. verify() 검증 시, eq("key") + any(Class) 조합을 사용하면 불필요한 매칭 실패를 방지할 수 있다.

 


참고 자료