-
CGLIB Proxy 와 Mockito 콜라보레이션 (feat. Not a Mock)JAVA/tips 2020. 7. 10. 01:00반응형
근황
최근 팀 이동으로 인해 갑작스럽게 자바 개발자가 되었다.
대한민국은... 어딜가나 자바를 해야한단 말인가... 😢
그래서 나름 열심히 스프링 공부를(?) 하고 있다.자바개발자가 아니라 스프링 개발자인듯
멘붕
갑작스럽게 프로젝트 하나를 맡아서 열심히 분석하고 개발하려고 봤는데
테스트코드가... 음...
이참에 기능 분석도 하고 리팩토링 준비도 할 겸 테스트 코드들을 작성하기 시작했다.
JUnit으로 테스트 코드는 아주 간단한 것들만 짜봤었는데 이번 기회에 Mockito도 써보고
생각보다 재미가 쏠쏠했다.그러다가 서비스 클래스의 테스트 코드를 작성하는데
안에서 다른 서비스 클래스를 참조하고 있었다.
그래서 참조되는 서비스 클래스를 스파이로 만들어서 호출여부만 확인하려고 했는데
이상한 에러가 발생했다.Argument passed to verify() is of type SmtpServiceImpl$$EnhancerBySpringCGLIB$$36c51d3f and is not a mock! Make sure you place the parenthesis correctly! See the examples of correct verifications: verify(mock).someMethod(); verify(mock, times(10)).someMethod(); verify(mock, atLeastOnce()).someMethod(); org.mockito.exceptions.misusing.NotAMockException: Argument passed to verify() is of type SmtpServiceImpl$$EnhancerBySpringCGLIB$$36c51d3f and is not a mock! Make sure you place the parenthesis correctly! See the examples of correct verifications: verify(mock).someMethod(); verify(mock, times(10)).someMethod(); verify(mock, atLeastOnce()).someMethod(); at io.huna.api.service.impl.NotificationServiceImplTest.test_sendFailNotification(NotificationServiceImplTest.java:54) ... (생략)
SmtpServiceImpl$$EnhancerBySpringCGLIB$$36c51d3f
이건 도대체 뭐지? 모양이 이상한데? 목이 아니라고?코드는 아래와 같았다.
@Autowired private NotificationService notificationService; @SpyBean private SmtpService smtpService; @Test public void test_sendFailNotification() { // given String message = "Exception occurred"; String stackTrace = "..."; // when notificationService.sendFailNotification(message, stackTrace); // then verify(smtpService, times(1)).sendMail(anyString(), anyMap(), anyString()); }
CGLIB (Code Generation Library)
일단 CGLIB가 뭔지 찾아봤다. yeti님의 블로그에 아주 친절하게 설명이 되어 있었다.
스프링에서는 두 가지 방식으로 프록시를 생성하는데
하나는 인터페이스를 통해 생성하는 Dynamic Proxy가 있고
다른 하나는 클래스에 생성하는 CGLIB 가 있다.그렇다. SmptServiceImpl이 프록시인데 CGLIB로 생성된 프록시라 이름이 저렇게 생겼던 것이다.
그렇다면 얘는 왜 목이 아닐까?
구글링
모르면 일단 구글에 물어봐야한다.
나의 선배님들도 분명 같은 문제로 골머리를 썩었을 것이고
거기에 대한 해답은 모두 구글을 통해 안내받을 수 있다. 🤩역시나 구글에서 해답을 찾을 수 있었다. 바로 여기!
영알못이 꾸역꾸역 읽어가며 알아낸 사실은 일단 프록시를 벗겨내라는 것이었다.
그런 뒤에ReflectionTestUtils
를 통해 프록시를 벗겨낸놈을 세팅하는 것이었다.유레카
스택오버플로우 출신 천재 답변가님의 코드를 베껴보자.
@Autowired private NotificationService notificationService; @Autowired private SmtpService smtpService; private <T> T unwrapProxy(T t) throws Exception { if (AopUtils.isAopProxy(t) && t instanceof Advised) { T target = (T) ((Advised) t).getTargetSource().getTarget(); return target; } return null; } @Test public void test_sendFailNotification() throws Exception { // given SmtpService smtpSpy = Mockito.spy(unwrapProxy(smtpService)); ReflectionTestUtils.setField(notificationService, "smtpService", smtpSpy); String message = "Exception occurred"; String stackTrace = "..."; // when notificationService.sendFailNotification(message, stackTrace); // then verify(smtpService, times(1)).sendMail(anyString(), anyMap(), anyString());
오......
우와.....
테스트케이스가 정상적으로 성공한다...디버깅 모드로 찍어봐도 신기하게
unwrapProxy
메소드를 통과하고 나면 프록시가 벗겨진채 놈으로 출력된다.
그건 그렇고 왜 목이 아니라고?
Tomasz Nurkiewicz 님의 말에 의하면The problem is quite complex, but solvable. As you have guessed this is a side-effect of CGLIB proxies being used. In principle, Spring creates a subclass of your FooServiceImpl named similar to FooServiceImpl$EnhancerByCGLIB. This subclass contains a reference to the original FooServiceImpl as well as... all the fields FooServiceImpl has (which is understandable - this is a subclass).
So there are actually two variables: FooServiceImpl$EnhancerByCGLIB.fooDao and FooServiceImpl.fooDao. You are assigning a mock to the former but your service uses the latter... I wrote about this pitfalls some time ago.
그렇단다. (음?)
골자는 다음과 같다.
- CGLIB proxy의 사이드 이펙트이다.
- 스프링이
FooServiceImpl$EnhancerByCGLIB
와 비슷한 이름의FooServiceImpl
의 서브클래스를 만든다. - 이 서브클래스는 원본
FooServiceImpl
의 모든 필드들의 참조도 가지고 있다. - 따라서
FooServiceImpl$EnhancerByCGLIB.fooDao
이거랑FooServiceImpl.fooDao
이거 두개가 존재한다. - 근데
FooServiceImpl$EnhancerByCGLIB.fooDao
이걸로 목을 만들었는데 서비스에서는FooServiceImpl.fooDao
이걸 쓴다... - 목키토 바보
역시 똑똑한 사람들은 너무 멋지다.
-끝-
반응형'JAVA > tips' 카테고리의 다른 글
abbreviate (줄여쓰기)를 정규표현식으로 구현해보기 (0) 2022.05.03 How to use Redis 5.0.14 in embedded-redis (0) 2021.11.25 RCP에서 AbstractUIPlugin 구현 클래스 자동으로 activate 설정 (0) 2017.10.18 How to logging in plugin development (0) 2016.05.12