verify() 용도
test 코드가 실행되는 동안 특정 메서드가 실행된 횟수등을 확인합니다.
E.g. verify(mock, times(5)).someMethod("was called five times");
사용하다보니 몇가지 주의사항이 있어서 글을 남깁니다.
아래 2가지 내용을 확인하세요.
빨리 확인해야한다면 '정리' 만 바로 확인하세요.
1. MockBean만 검증할 수 있다.
실무에서 이미 작성된 테스트코드에 추가로 verify() 검증을 추가해봅니다.
근데.. 에러가 발생합니다
Argument passed to verify() is of type PointService$$EnhancerBySpringCGLIB$$2aeafde9 and is not a mock!
verify()로 전달된 매개변수가 mock이 아니라네요.
아래는 에러가 발생한 코드입니다

- 이 테스트는 Spring 전체 테스트입니다. @SpringBootTest 어노테이션이 붙어있는 것을 보면 알 수 있습니다.
테스트 실행 시 마치 실제 실행하는 것 처럼 모든 빈을 등록하고, 주입해줍니다
그리고@Autowired어노테이션을 이용했으므로, 스프링 빈으로 등록되어있을PointService클래스를 주입받았습니다. - 그리고
PointService클래스의 findAll() 메서드가 1번만 제대로 실행됐는지 verify()로 확인하는 로직을 작성했습니다. - 테스트는 실패했고, 앞서 설명했던 에러메시지를 확인 할 수 있습니다.
에러 메시지를 찬찬히 읽어보면 알 수 있습니다. verify()에서 체크할 수 있는 클래스는 Mock 빈이어야 합니다!
이제서야 알았네요.
해결방법? - @MockBean
그렇담 Mock으로 만들어주면 되죠.

verify()로 검증 할 클래스를 MockBean으로 만들고 테스트를 실행했더니 성공했습니다!
하지만, 이 테스트는 반쪽짜리 해결이었습니다.
2. @MockBean 으로 등록한 클래스 내부의 로직은 검증 할 수 없다
그렇게 코드를 푸시했고, 완료한 줄 알았습니다.
근데, QA가 진행되던 와중에 위에서 테스트코드로 검증했던 로직에서 에러가 발생했습니다.
이유는 MockBean인 PointService 클래스의 내부 메서드가 모두 실행되지 않고 넘어가서 잠재된 에러를 발견하지 못했기 때문입니다.
즉, PointService.withdrawalMembershipProccess() 메서드 내부 로직이 test-code에서는 아예 실행되지 않았던 겁니다.
그래서 실제로 메서드가 실행됐더라면 발견했을 Exception 지점을 놓쳤습니다.
그럼 어떻게 해야할까요?
기존의 verify() 검증하는 test-code 외에 추가로 실제 메서드가 호출되어 로직이 실행되는 test-code를 별도로 짜야할까요?
….
해결방법! - @SpyBean
간단했습니다. @SpyBean 어노테이션으로 바꾸기만하면 됩니다.
⚠️ 참고
PointService.withdrawalMembershipProccess()가 실행되면 "탈퇴회원 {}의 포인트 회수 프로세스 진행" 메시지가 로그에 찍혀야 한다.
public class PointService { public void withdrawalMembershipProccess(Member member) { log.info("탈퇴회원 {}의 포인트 회수 프로세스 진행", member.getName()); } }

@SpyBean어노테이션으로 바꿔서 테스트 실행했다.- verify() 검증대상의 메서드 내 로직이 제대로 실행됐다 : 로직 내 작성된 텍스트가 출력됐다.
- verify() 테스트도 통과했다.
이로써 테스트하려는 메서드 내 로직도 검증하고, verify()로 실행횟수도 검증을 할 수 있습니다.
정리
JUnit 테스트코드를 짤 때 verify()를 사용하려면, 검증할 객체는 ‘Mock Bean’이어야한다.
하지만, 단순히 @MockBean으로 선언해버리면 그 내부의 로직이 테스트 검증이 되지 않으므로, @SpyBean을 이용해서 로직도 검증하고, verify()도 검증한다.
아래부터는 뭐가 다른지 궁금해서 좀 더 파본 내용이다.
굳이 안봐도 되는 내용이니, @SpyBean, @MockBean, @Autowired에 대해 삽질을 해보고 싶지 않다면 여기서 글 읽기를 종료해도 된다.
🔥 좀 더 파보기
1. 왜 @SpyBean, @MockBean 로 생성된 Mock 객체만 verify() 검증이 가능할까?
첫번째 알아볼 내용은 1번으로 작성한 1. MockBean만 검증할 수 있다. 과 관련된다.
@SpyBean, @MockBean, @Autowired 로 Bean을 생성하면 어떻게 다른가 확인해보자
일단 3개 어노테이션이 어떻게 다른 객체를 만드는지 확인해보자
1. @Autowired
- 객체가 SpringCGLIB로 한 번 감싸있다.

2. @MockBean 일 때
- 객체가 MockitoMock으로 한 번 감싸있다.

3. @SpyBean 일 때
- 객체가 MockitoMock, SpringCGLIB로 두 번 감싸있다.

정리
| @Autowired | @MockBean | @SpyBean |
| 객체가 SpringCGLIB로만 한 번 감쌌다. | 객체가 MockitoMock으로만 한 번 감쌌다. | 객체가 MockitoMock, SpringCGLIB로 두 번 감쌌다. |
🧐 아마 SpyBean을 사용했을 때 내부로직이 실행되는 이유는 SpringCGLIB 와 관련있는 것 같다(?왜지?)
verify() 메서드 열어보기
MockingDetails.isMock() 가 있다.
이 부분에서 Mock 객체가 아닐시 위에서 봤던 이런 에러가 발생하는 것이다.
Argument passed to verify() is of type PointService$$EnhancerBySpringCGLIB$$2aeafde9 and is not a mock!
즉, 이 부분이 3개 어노테이션으로 생성된 객체가 Mock 객체인지 확인하는 부분이니, 다음 단계에서 살펴보자.

isMock()을 열어보자
그럼 isMock()을 열어봅시다.

여기서는 크게 2개 로직이 있습니다.
1. resolve(mock)
SpringProxy인 SpringCGLIB로 감싸져있던 mock의 Proxy를 벗기고, 실제 객체를 리턴합니다.
(resolve() 메서드 내부도 열어봤지만 그 것까지 쓰면 글이 장황해져서 생략한다)
어노테이션 별로 resolve() 전/후를 정리해보면 아래와 같습니다.
| @Autowired | @MockBean | @SpyBean | |
| 객체가 SpringCGLIB로만 한 번 감싸있다. | 객체가 MockitoMock으로만 한 번 감싸있다. | 객체가 MockitoMock, SpringCGLIB로 두 번 감싸있다. | |
| 해체전 | [[PointService] SpringCGLIB] | [[PointService] MockitoMock] | [[[PointService] MockitoMock] SpringCGLIB] |
| 해체후 | [PointService] | [[PointService] MockitoMock] | [[PointService] MockitoMock] |
해체후를 보면 @MockBean과 @SpyBean 객체의 결과가 동일합니다. 즉, MockitoMock 객체가 반환됩니다
2. return mockMaker.getHandler(mock)
mockReposity에서 mock(키)로 handler(값)을 찾습니다.
즉, @MockBean과 @SpyBean 객체만 true가 반환됩니다.
정리: Mock 객체여야하는 이유
일단 verify() 내부에서 isMock()으로 Mock객체여부를 체크하기 때문이었다.
Mock객체여부를 체크하는 방법은, verify() 검증 대상 인스턴스가 MockitoMock로 감싸진 객체인지 체크한다
실제 객체가 주입되는 @Autowired는 MockitoMock로 감싸지지 않기 때문에 Exception이 발생한 것이다.
그렇다면 왜? 굳이 걸러낼까?
거기까지는 열어보진 않았지만, 당연히 MockitoMock 객체여야 verify()에서 필요한 검증을 진행할 수 있기 때문일거다.
내부 상세 로직은 굳이 열어보지 않아도 될 것 같다.
2. @SpyMock은 어떻게 실제 로직이 실행되나? SpringCGLIB로 감싼 것과 무슨 연관이 있나?
음.. 이건 앞서 의문이었던 jdkDynamicAopProxy와 SpringCGLIB의 차이를 먼저 확인하다보면 알게될 것 같다.
또한 SpringCGLIB의 기능을 알아보면 좋을 것 같다.
확실한건 @SpyMock는 SpringCGLIB의 기능을 이용한 어노테이션이라는 것이다!
'삽질인가 고찰인가' 카테고리의 다른 글
| 변수, 타입 추론 VS 명시적 타입 지정. 그동안 명시적 타입을 선호했는데... '타입 추론'을 지향해야 한다고? (1) | 2025.01.31 |
|---|---|
| java의 stream에 대하여. 책을 읽어도 이해 안되는거 나만...인가? Stream 동작방식 쉽게 설명해보기. (1) | 2025.01.30 |
| Git 명령어) 중간 commit 삭제하기 (0) | 2024.09.20 |
| Python 클래스 메서드, 클래스 속성, 정적 메서드. 개념 구분/용도/주의사항. 언제 어떻게 쓸까? (1) | 2024.08.08 |
| Transaction살펴보기1) 엥 왜 롤백이 되지? Transaction silently rolled back because it has been marked as rollback-only. (0) | 2023.11.16 |