1. Mockito의 spy란
1-1. spy가 필요한 상황
1-2. mock 과 spy의 차이
2. spy와 사용하는 대표 스터빙 함수
2-1. doReturn
2-2. doAnswer
2-3. doThrow
3. spy 사용 시 주의 사항
1. Mockito의 spy란
실제 객체를 감싸는 래퍼로 실제 로직을 그대로 수행하면서도 특정 메서드만 선택적으로 가짜(Stub)로 만들거나 호출 기록을 감시할 수 있는 Mockito의 객체.
테스트 코드를 작성하다 보면 mock객체가 아닌 실제 객체가 사용하는 메서드에 대해 stub 해야 할 상황이 발생하고
그것을 해결하기 위해 사용한다.
* stub이란 테스트에서 특정 메서드 호출에 대해 미리 정의된 결과나 동작을 반환하도록 만든 테스트 기법.
stub의 목적은 테스트 대상 외부 요소의 영향을 차단하고 테스트 대상의 로직을 안정적으로 검증하는 데 있음.
1-1. spy가 필요한 상황
1. 도메인 엔티티에 대한 단위 테스트를 진행할 때
2. 테스트 대상이 자기 자신의 로직일 때
3. 의존성 주입이 어려운 클래스에 대해 테스트할 때 등
1-2. mock과 spy의 차이
| 구분 | Mock | spy |
| 본질 | 완전히 가짜 객체 | 실제 객체를 감싼 객체 |
| 기본 동작 | 모든 메서드가 가짜 | 모든 메서드가 실제 실행 |
| stub 없을 때 | 아무 일도 안 함 | 실제 로직 실행 |
| 테스트 목적 | 협력 객체 대체 (의존 주입 대상) | 테스트 대상 보존 |
* 디버깅 할 때 포인트 찍으면 Mock객체 메서드도 호출부에 걸리긴 하나 내부로 들어가진 않으니 헷갈리지 말자.
2. spy와 사용하는 대표 stub 함수
2-1. doReturn
코드 예시
@Test
void 가입_신청_승인_실패_권한X_SPY() {
// given
StudyMember normalMember = StudyMember.createMember(samplestudyGroup, 1L);
// stub: spyGroup의 findStudyMemberByMemberId 메서드를 호출하면 normalMember를 리턴
StudyGroup spyGroup = spy(samplestudyGroup);
doReturn(normalMember)
.when(spyGroup)
.findStudyMemberByMemberId(any());
System.out.println("================================");
// when, then
DomainException ex = assertThrows(DomainException.class, () -> {
spyGroup.joinMember(2L, 1L);
});
assertEquals(ErrorCode.HAS_NOT_PERMISSION, ex.getErrorCode());
}
설명
- 일반 회원이 가입 신청 승인할 때 예외가 터지는 걸 확인하고 싶은 테스트
- StudyGroup의 findStudyMemberByMemberId() 가 일반 회원을 리턴한다고 미리 정의한다.
- 일반 회원이 가입 신청 승인 메서드 호출 ⇒ 예외 발생
- StudyMember의 joinMember 안에서 findStudyMemberByMemberId()를 호출하기 때문에 해당 값을 스터빙하여 진행함.

- findStudyMemberByMemberId()에도 찍어놓았지만 실제 로깅은 되지 않음(스터빙하여 실제 호출X)
- 나머지 joinMember() 안에서 실행되는 메서들은 모두 로깅이 찍힘(실제 메서드가 호출되므로)
2-2. doAnswer
@Test
void 가입_신청_승인_성공_권한검증무시_SPY() {
// given
StudyGroup spyGroup = spy(samplestudyGroup);
// stub: spyGroup의 validJoinMember 메서드를 호출하면 아무 동작을 하지 말아라
doAnswer(inv -> null)
.when(spyGroup).validJoinMember(any());
// when & then
assertDoesNotThrow(()-> spyGroup.joinMember(2L, 1L));
}
설명
- spy 객체의 메소드 동작 자체를 테스트 상황에 따라 동적으로 바꾸고 싶은 상황.
- 원래라면 validJoinMember메서드가 호출되며 예외가 터져야 하지만, 해당 메서드에 대한 동작을 null로 스터빙하여 예외 발생X
2-3. doThrow
doThrow(new RuntimeException(..))
.when(spy).method(any());
3. spy 사용 시 주의 사항
3-1. 실제 메서드가 실행된다.
위에서도 말했지만, 해당 부분이 가장 중요함.
객체의 메서드 흐름이 실제로 이어지기 때문에
- DB 접근
- 네트워크
- 의존성 메서드
등등 의도치 않은 문제가 발생할 수 있어 위에서 말한 stub의 본질을 크게 흐릴 수 있음.
stub의 목적은 테스트 대상 외부 요소의 영향을 차단하고 테스트 대상의 로직을 안정적으로 검증하는 데 있음.
3-2. when then 절과 같이 사용하면 안 된다.
2-1에서 doReturn() 보면 자연스럽게 when then절이 생각날 것이다. 실제로 의미적인 동작은 동일하다.
근데 내부 동작에 있어 치명적인 차이가 있다.
when절은 작성만으로 해당 메서드를 실제로 호출해 버린다.
이때 mock객체는 when절 같이 쓰지 않나 뭐가 문제라고 생각할 수 있지만 mock 객체는 해당 메서드를 실제로 호출하지 않는다.
코드 예시 (위의 2-1 코드와 stub 하는 부분만 다르고 나머진 같음)
@Test
void 가입_신청_승인_실패_권한X_SPY_WITH_WHENTHEN(){
// given
StudyMember normalMember = StudyMember.createMember(samplestudyGroup, 1L);
StudyGroup spyGroup = spy(samplestudyGroup);
when(spyGroup.findStudyMemberByMemberId(1L)).thenReturn(normalMember);
System.out.println("==================================================");
// when & then
DomainException ex = assertThrows(DomainException.class, () -> {
spyGroup.joinMember(2L, 1L);
});
assertEquals(ErrorCode.HAS_NOT_PERMISSION, ex.getErrorCode());
}
설명
- 기존 코드와 스터빙 함수만 doReturn → when thenReturn절로 바꾸었으며 결과도 동일하다.
- when() thenReturn()은 실행만으로 스터빙 함수를 실제 호출한다.
- 아래 콘솔 로깅을 보면 스터빙 단계인 when절에서 실제 메서드를 호출하는 게 확인된다.
- 결국 원래 목적을 위해 stub을 하였는데 stub 자체가 테스트에 영향을 끼치게 되니 절대 같이 사용하지 말자.

3-3. spy 과다 사용 금지
위에서 설명한 대로 spy는 테스트 본연의 본질을 흐릴 가능성이 높고 의도치 못한 문제를 많이 발생시킨다.
그렇기 때문에 stub이 필요한 상황이 많다면 좋지 않고
해당 객체의 역할이 너무 과중하지 않은지, 설계는 괜찮은지 의심해봐야 한다.
'Spring > junit5 테스트' 카테고리의 다른 글
| [스프링부트] junit 테스트 코드(5/5) - 예외 테스트 (0) | 2023.08.08 |
|---|---|
| junit 테스트 코드(4/5) - 통합테스트 (0) | 2023.08.01 |
| junit 테스트 코드(3/5) - 단위 테스트(RepositoryLayer) (0) | 2023.07.31 |
| junit 테스트 코드(1/5) - 개념 및 용어 (2) | 2023.07.30 |