포스트

Issue - org.mockito.exceptions.verification.VerificationInOrderFailure: Verification in order failure: redisTemplate.opsForSet();

발생 이슈

RedisTemplate을 사용하는 Service의 테스트 코드를 작성하던 중 redisTemplate.opsForSet().add() 부분에서 1번만 호출되어야 했을 redisTemplate.opsForSet() 메서드가 2번 호출되었다는 에러 메세지를 계속 받게 되었다.

에러 메세지

org.mockito.exceptions.verification.VerificationInOrderFailure: 
Verification in order failure:
redisTemplate.opsForSet();
Wanted 1 time:
-> at org.springframework.data.redis.core.RedisTemplate.opsForSet(RedisTemplate.java:1016)
But was 2 times:
-> at kim.zhyun.serveruser.domain.signup.service.EmailServiceTest.save_email_auth(EmailServiceTest.java:102)
-> at kim.zhyun.serveruser.domain.signup.service.EmailService.saveEmailAuthInfo(EmailService.java:46)


    at org.springframework.data.redis.core.RedisTemplate.opsForSet(RedisTemplate.java:1016)
    at kim.zhyun.serveruser.domain.signup.service.EmailServiceTest.save_email_auth(EmailServiceTest.java:112)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:728)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:218)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:214)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:139)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
    at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:119)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:94)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:89)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)


에러 발생 코드 일부

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@InjectMocks EmailService emailService;
@Mock RedisTemplate<String, String> redisTemplate;

@Test
void redis_template_add() {
    // given
    SetOperations setOperations = mock(SetOperations.class);
    given(redisTemplate.opsForSet()).willAnswer(invocation  -> setOperations);
    given(redisTemplate.opsForSet().add(emailAuthDto.getEmail(), emailAuthDto.getCode())).willReturn(1L);
    given(redisTemplate.expire(eq(emailAuthDto.getEmail()), anyLong(), eq(TimeUnit.SECONDS))).willReturn(true);
    
    // when
    emailService.saveEmailAuthInfo(emailAuthDto);
    
    // then
    InOrder order = inOrder(redisTemplate);
    order.verify(redisTemplate, times(1)).opsForSet().add(emailAuthDto.getEmail(), emailAuthDto.getCode());
    order.verify(redisTemplate, times(1)).expire(eq(emailAuthDto.getEmail()), anyLong(), any(TimeUnit.class));
}


✏️

Mocking과 Stubbing에 대한 개념 다시 짚고 넘어가기

Mocking

실제 객체를 대체하여 특정 동작을 시뮬레이션 할 수 있도록 가짜 객체(Mock 객체)를 생성하는 행위.
stubbing을 통해 Mock 객체의 메서드 호출 시 수행되는 동작을 정의해줌으로써 시뮬레이션을 진행한다.

Stubbing

Mock객체의 메서드가 호출 될 때 어떤 동작을 수행할 지 정의하는 행위


해결

검증하는 부분에서 redisTemplate.opsForSet().add(); 코드를 다시 작성해야 한다.

stubbing이 redisTemplate 목 객체에 대한 redisTemplate.opsForSet()과 SetOperations 목 객체에 대한 SetOperations.add();로 되기 때문에 검증 또한 같은 형태로 이루어져야 한다.

수정 된 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@InjectMocks EmailService emailService;
@Mock RedisTemplate<String, String> redisTemplate;

@Test
void redis_template_add() {
    // given
    SetOperations setOperations = mock(SetOperations.class);
    given(redisTemplate.opsForSet()).willAnswer(invocation  -> setOperations);
    given(redisTemplate.opsForSet().add(emailAuthDto.getEmail(), emailAuthDto.getCode())).willReturn(1L);
    given(redisTemplate.expire(eq(emailAuthDto.getEmail()), anyLong(), eq(TimeUnit.SECONDS))).willReturn(true);
    
    // when
    emailService.saveEmailAuthInfo(emailAuthDto);
    
    // then
    InOrder order = inOrder(redisTemplate, setOperations);
    order.verify(setOperations, times(1)).add(emailAuthDto.getEmail(), emailAuthDto.getCode());
    order.verify(redisTemplate, times(1)).expire(eq(emailAuthDto.getEmail()), anyLong(), any(TimeUnit.class));
}


발생 원인 분석

1. Method Chaining과 Mockito 이해 부족

RedisTemplate 코드를 자연스럽게(?) Method Chaining 형태로 사용했기 때문에 redisTemplate.opsForSet().add() 코드를 하나의 메서드를 실행하는 것처럼 mocking하려고 했었다.
그리고 mock 객체에서 사용하려는 메서드는 전부 stubbing 해주어야 한다.


2. stubbing 과정에서

위에 작성한 에러 발생 코드도 사실은 한번의 Mockito 에러를 경험 한 후 수정된 코드였다.
RedisTemplate을 Mocking 할 때 RedisTemplate만 Mocking 하면 되는 줄 알고 처음엔 다음과 같이 코드를 작성했었다.

1
2
3
4
5
6
7
8
9
@InjectMocks EmailService emailService;
@Mock RedisTemplate<String, String> redisTemplate;

@Test
void redis_template_add() {
    // given
    given(redisTemplate.opsForSet().add(emailAuthDto.getEmail(), emailAuthDto.getCode())).willReturn(1L);
    given(redisTemplate.expire(eq(emailAuthDto.getEmail()), anyLong(), eq(TimeUnit.SECONDS))).willReturn(true);
    

이렇게 작성하면
given(redisTemplate.opsForSet().add(emailAuthDto.getEmail(), emailAuthDto.getCode())).willReturn(1L); 코드에서 redisTemplate.opsForSet()이 null이라는 Mocktio Exception을 만나게 된다.

그렇기 때문에 redisTemplate.opsForSet()의 반환 형태인 SetOperations 객체를 Mocking 하여 Stubbing 해주어야 했다.

Mock 객체가 Interface처럼 실제 객체의 껍데기만 들어있는 형태이기 때문에 당연한 결과였다.

그래서 이 에러를 해결하기 위해 SetOperations 객체를 Mocking 해주었고 결과적으로 redisTemplate.opsForSet().add(); 기능은 SetOperations Mock 객체에 stubbing 되어야 한다.

이 부분을 이해하지 못한채로 진행했었기 때문에 오전에 해결했던 이 문제를 저녁에 다시 한 번 경험하고 말았다 😳


3. 검증 과정에서

나의 경우 InOrder를 통해서 Mock객체의 실행 순서를 검증하고, 해당 메서드가 1회만 실행 되었는지 확인하고자 코드를 작성했었는데 mock 객체를 redisTemplate으로 설정하고 redisTemplate.opsForSet.add(); 코드에 대해 검증을 진행하려고 해서 redisTemplate이 2번 실행되었다는 에러 문구를 받게 되었다.

에러를 해결하기 전까지는 이 코드에서 redisTemplate이 왜 2번 실행된다는건지 전혀 이해가 가지 않았다.
chatGPT와 대화를 나누며 원인을 어림짐작 할 수 있었고 덕분에 좀 더 mocking에 대해 이해 할 수 있게 되었다.

내 경우 add() 메서드가 1번 실행되는지가 중요했기 때문에 mock 객체를 SetOperations로 수정하고 실행 메서드를 SetOperations.add(); 형태로 변경해주어 1번 실행됨을 확인하면서 의도한 결과를 얻을 수 있었다.




참고한 사이트

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.