ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [트러블 슈팅] @InjectMocks @Mock vs. @Autowired @MockBean
    Spring 2024. 6. 17. 06:42

     

     

    단위 테스트를 구현하다가 아래와 같은 오류를 만났다.

    com.rollthedice.backend.domain.news.exception.NewsNotFoundException: 뉴스를 찾지 못했습니다. at java.base/java.util.Optional.orElseThrow(Optional.java:403) at com.rollthedice.backend.domain.news.service.NewsService.getDetailNews(NewsService.java:75) at

     

     

    ⬇️ 오류 로그 전체

    더보기
    com.rollthedice.backend.domain.news.exception.NewsNotFoundException: 뉴스를 찾지 못했습니다. at java.base/java.util.Optional.orElseThrow(Optional.java:403) at com.rollthedice.backend.domain.news.service.NewsService.getDetailNews(NewsService.java:75) at com.rollthedice.backend.domain.news.service.NewsServiceTest.getDetailNews(NewsServiceTest.java:81) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) 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:217) at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) 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.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) 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)

     

     

     

    [ NewsService의 getDetailNews 메서드 ]

    public NewsDetailResponse getDetailNews(Long newsId) {
        final News news = newsRepository.findById(newsId).orElseThrow(NewsNotFoundException::new);
        readNewsRepository.save(ReadNews.builder()
                .member(authService.getMember()).news(news).build());
        return newsMapper.toDetailResponse(news);
    }

     

        @Test
        @DisplayName("뉴스를 상세조회할 수 있는가")
        void getDetailNews() {
            //given
            NewsDetailResponse expect = NEWS_DETAIL_RESPONSE();
            Long newsId = expect.getId();
    
            given(readNewsRepository.save(any(ReadNews.class))).willReturn(ReadNews.builder().build());
            given(newsMapper.toDetailResponse(news)).willReturn(expect);
            
            //when
            NewsDetailResponse result = newsService.getDetailNews(newsId);
    
            //then
            assertThat(result).isEqualTo(expect);
        }

     

     

     

    [ 1차 시도 ] anyLong()

    처음에는 given(newsRepository.findById(newsId)).willReturn(Optional.of(news)); 에서 newsId에 문제가 있다고 생각했다.

    NewsFixture에서 news 객체에 newsId를 지정하지 않았기 때문에 특정 newsId와 매칭이 되지 않아 이러한 오류가 발생한 것이 아닌가? 라는 단순한 생각이었다.

    그래서 newsId를 아래 코드와 같이 anyLong()으로 변경했다.

    given(newsRepository.findById(anyLong())).willReturn(Optional.of(news));

     

    하지만 여전히 같은 오류가 발생했다.

     

     

     

    [ 2차 시도 ] when().thenReturn()

    이전에 when().thenReturn()을 사용한 경험이 있었다. given().willReturn()과 비슷한 기능을 제공하는 것 같지만, when()은 Mockito에서, given()은 BDDMockito에서 제공하는 메서드라는 차이가 있었다.

     

    그래서 혹시나하고 적용해 보았다.

    when(newsRepository.findById(newsId)).thenReturn(Optional.of(news));
    

     

    하지만 여전히 같은 오류가 발생했다.

     

     

    그리고 더 알아보니, BDDMockito는 Mockito를 상속한 클래스이며, given().willReturn()과 when().thenReturn()은 같은 기능을 제공한다고 볼 수 있을 정도로 유사했다. 또한, BDDMockito는 given, when, then에 맞춰 테스트를 작성할 수 있어, 테스트 코드의 가독성을 향상시키기 위한 목적으로 만들어졌다고 한다.

     

    [ 3차 시도 ] 의존성 주입 방법 변경: @Autowired & @MockBean

     

    명상 한 번 했다.

     

    다음 코드를 실행했을 때, 정상적으로 로그가 찍히는 것을 확인했다.

    log.info("id : " + newsRepository.findById(id).get().getId());

     

     

    그렇다면 문제는 실제 메인 서비스 코드나 테스트 관련 설정에 있을 것이라고 생각했다.

    하지만, 아래의 코드에서 문제는 전혀 보이지 않는다. 아주 간단하고, 항상 사용하던 형식의 코드이다.

    final News news = newsRepository.findById(newsId).orElseThrow(NewsNotFoundException::new);
    

     

    그렇다면, 설정과 관련된 문제라고 예상했다.

     

    성공!!!!!!!!!!!!!!!

     

    의존성 주입 방식이 문제였다..

    @Slf4j
    @DisplayName("NewsService의")
    @ExtendWith(MockitoExtension.class)
    class NewsServiceTest extends LoginTest {
    	@Autowired
    	private NewsService newsService;
    	@MockBean
    	private NewsRepository newsRepository;
    	@MockBean
    	private ReadNewsRepository readNewsRepository;
        
            private News news;
    
        @BeforeEach
        void setUp() {
            news = NEWS(MEMBER());
        }
        
        @Test
        @DisplayName("뉴스를 상세조회할 수 있는가")
        void getDetailNews() {
            //given
            NewsDetailResponse expect = NEWS_DETAIL_RESPONSE();
            Long newsId = expect.getId();
    
            given(newsRepository.findById(newsId)).willReturn(Optional.of(news));
    
            //when
            NewsDetailResponse result = newsService.getDetailNews(newsId);
    
            //then
            assertThat(result.getId()).isEqualTo(expect.getId());
        }
    }

     

     

    이렇게 변경해주었더니 테스트를 통과한다..

     

     

    주요 차이점

    1. 의존성 주입 방식:
      • : @InjectMocks와 @Mock을 사용하여 필드 주입을 수행
      • : @Autowired와 @MockBean을 사용하여 Spring의 컨텍스트에서 필드 주입을 수행
    2. Spring 환경 적용 여부:
      • : Mockito의 @InjectMocks, @Mock은 Spring 컨텍스트를 구성하지 않고, Mockito만을 사용하여 테스트를 수행
      • : @Autowired와 @MockBean은 Spring의 통합 테스트 환경을 제공하여, 실제 Spring 빈을 주입하고, @MockBean은 목 객체를 주입하여 통합 테스트를 수행

     

    이유 분석

    나는 로그인한 사용자 정보를 가져올 때, Controller에서 파라미터를 통해 받아오는 방식이 아닌, 아래와 같은 AuthService 를 서비스 코드에 의존성 주입하여 autherService.getLoginUser()와 같은 방식으로 가져온다. 즉, AuthService에서는 컨텍스트 홀더에서 직접 로그인한 사용자를 가져오는 방식이다.

    @Slf4j
    @QueryService
    @RequiredArgsConstructor
    public class AuthService {
        private final MemberRepository memberRepository;
    
        public Long getLoginUserId() {
            return getLoginUser().getId();
        }
    
        public Member getLoginUser() {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            return memberRepository.findByEmail(userDetails.getUsername()).orElseThrow(EntityNotFoundException::new);
        }
    }

     

    이와 같은 이유로, ServiceTest에서 상속받은 LoginTest는 아래와 같이 SpringBootTest이며, AuthService를 @Autowired로 의존성 주입을 하고있다. 따라서 사용자 정보를 가져오기 위해서는 Spring의 컨텍스트에서 의존성을 주입한 AuthService를 통해 가져와야 하는 것이다..!

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
    

     

     

    결론

    AuthService를 서비스 클래스에서 주입받아 사용한 이유는, 컨트롤러에서 호출하는 서비스 메서드의 파라미터 수를 줄이기 위함이었다. 하지만, 그 결과로 테스트 용이성이 떨어졌다. 또한, 단위 테스트의 의미도 약간은 흐려진 것 같다.

    테스트 용이성과 파라미터 수 줄이기(가독성에 큰 영향을 준다고 생각한다.) 중에 무엇을 선택해야 할지 좀 더 고민해봐야 할 것 같다.

     

Designed by Tistory.