🤔 단위 테스트(Unit Test)란?
단위 테스트(Unit Test) 는 소프트웨어 개발에서 가장 기본적인 테스트 기법으로, 특정 기능 단위(보통 메서드)의 동작을 검증한다
🤔 Mockito란?
Mockito는 테스트 대상 객체의 의존성을 모방(mock)하는 가짜 객체를 쉽게 생성해 주는 라이브러리이다.
실제 복잡한 의존성(DB, 네트워크, 외부 API)을 대신해, 테스트 환경을 단순화하고 빠르게 만들어 준다.
소프트웨어 개발 시 단위 테스트는 필수지만, 우리가 테스트하려는 클래스(예: MemberService)는 보통 이런 특징이 있다.
- 테스트 대상 클래스가 다른 객체에 강하게 의존 (예: MemberRepository)
- 의존 객체는 복잡하거나 무거움 (DB 연결, 네트워크 호출, 외부 API 등)
- 실제 객체 사용 시 테스트가 느리고, 실패 가능성이 많고, 독립적이지 않음
- 예를 들어, 서비스가 DB에 저장하려고 하면 테스트할 때마다 DB 세팅, 초기화가 필요해 비효율적
Mock = 테스트를 위한 '가짜' 객체
테스트 시 의존하는 복잡한 객체 대신 간단한 가짜 객체(Mock Object)를 사용해 원하는 동작을 흉내 내자!
이런 요구를 충족시키는 프레임워크가 바로 Mockito이다.
✏️ Mockito의 특징
- 진짜 객체와 똑같은 인터페이스를 가지고 있음
- 동작(메서드 호출)에 대해 원하는 대로 '행동'을 정해줄 수 있음
- 데이터베이스, 네트워크 등 외부 의존 없이 테스트 가능
🌟 Mockito 동작 원리
- Mockito는 Java의 프록시(Proxy) 기술을 사용해 런타임에 인터페이스나 클래스의 가짜 객체를 만든다.
- 이 가짜 객체는 메서드 호출을 가로채서, 미리 지정된 행동(결과 반환, 예외 발생 등)을 수행한다.
📚 주요 용어
용어 | 설명 |
Mock | 테스트 대상 의존 객체를 가짜로 만든 객체. 실제 객체 대신 사용하여 메서드 호출 시 원하는 동작을 지정 가능. |
Stub | Mock과 비슷하지만, 주로 메서드의 반환값을 미리 정의하는 데 집중. |
Spy | 실제 객체를 감싸면서 특정 메서드만 가짜 동작으로 변경 가능. 일부는 실제 동작, 일부는 모킹. |
Verification | 테스트 중 특정 메서드가 호출되었는지, 몇 번 호출되었는지 검증하는 과정. |
📚 Mockito, Spring Test 관련 어노테이션
// Mockito 확장자 등록 - Mockito 어노테이션 활성화
@ExtendWith(SpringExtension.class)
class MemberServiceTest {
// 의존하는 MemberRepository를 가짜(Mock) 객체로 생성
@Mock
private MemberRepository memberRepository;
// 테스트 대상 서비스 클래스
@InjectMocks
private MemberService memberService;
}
어노테이션 | 용도 |
@Mock | 가짜(Mock) 객체를 생성하여 테스트에서 사용. |
@InjectMocks | 테스트 대상 객체에 @Mock 객체들을 주입. |
@ExtendWith(MockitoExtension.class) | Mockito 기능 활성화 (JUnit5 환경에서 필요). |
@SpringBootTest | Spring 컨텍스트를 띄워 통합 테스트 시 사용. |
@WebMvcTest | 컨트롤러만 테스트할 때, Spring MVC 컨텍스트만 로드. |
@MockBean | Spring 컨텍스트 내에서 Mock 빈으로 등록할 때 사용. |
📚 Mockito 주요 메서드
1. when(...).thenReturn(...)
// memberRepository.findById(1L) 가 호출되면, 실제 DB 조회 없이 Optional.of(new Member("홍길동"))를 반환한다는 의미
when(memberRepository.findById(1L)).thenReturn(Optional.of(new Member("홍길동")));
- 특정 메서드가 호출될 때 미리 지정한 값을 반환하도록 설정
- 테스트 중에 실제 동작 대신 예상 결과를 미리 만들어 놓는 것
2. verify(...)
// memberRepository.save()가 최소 한 번 호출됐는지 검증
verify(memberRepository).save(any(Member.class));
// delete() 메서드가 정확히 2번 호출됐는지 검증
verify(memberRepository, times(2)).delete(any());
// save()가 한 번도 호출되지 않았는지 검증
verify(memberRepository, never()).save(any());
- 특정 메서드가 테스트 중에 호출됐는지, 몇 번 호출됐는지를 검증
- 테스트에서 메서드 호출 여부를 체크해 기대 동작이 일어났는지 확인하는 용도
3. any()
// 어떤 이메일이 들어와도 항상 빈 결과를 반환
when(memberRepository.findByEmail(any())).thenReturn(Optional.empty());
// 타입이 Member인 인자면 모두 허용해 호출 검증
verify(memberRepository).save(any(Member.class));
- 메서드 호출 시 전달된 인자값을 무시하고, 어떤 값이라도 허용하는 matcher(매처)
- Mockito의 매처(matcher)는 특정 인자에 국한하지 않고 호출 검증 및 동작 지정 시 편리하게 사용하다.
- 단, Mockito 매처 사용 시, 모든 인자에 대해 매처를 사용하거나 모두 실제 값이어야 하며 혼용 불가하다.
4. thenAnswer(...)
// save() 호출 시 입력받은 Member 객체에 ID를 세팅하고 그 객체를 반환하는 동작을 구현한 것
when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> {
Member arg = invocation.getArgument(0);
arg.setId(100L); // 저장 후 ID 부여하는 동작 흉내
return arg;
});
- 메서드 호출 시 단순히 값을 반환하는 것을 넘어서 호출된 인자값을 이용해 커스텀 동작을 구현
- 복잡한 로직이나 호출에 따른 결과를 동적으로 처리할 때 사용한다.
❗️ Mockito 매처 혼용 금지 규칙
someMock.someMethod(arg1, arg2, arg3);
Mockito의 메서드 스텁(stubbing)이나 검증(verification)을 할 때, 메서드 파라미터가 여러 개라면
이 중 일부 파라미터에는 매처(any(), eq(), anyString() 등)를 쓰고, 나머지에는 직접 실제 값을 쓰는 게 허용되지 않는다.
// ❌ 이렇게 매처와 실제 값이 섞여 있으면 오류 발생!
when(someMock.someMethod(any(), "hello")).thenReturn(something);
Mockito는 내부적으로 매처를 사용하는 인자와 실제 값을 구분해서 처리한다. 매처가 쓰인 인자는 Mockito가 알아서 매칭 작업을 해주고실제 값이 쓰인 인자는 그냥 값 자체로 비교하는데, 이 둘이 섞이면 Mockito가 내부적으로 어떤 비교를 해야 할지 혼란이 생겨 오류가 발생하기 때문이다.
✅ 올바른 방법
// 1. 모든 인자에 매처 사용
when(someMock.someMethod(any(), anyString())).thenReturn(something);
// 2. 모든 인자에 실제 값 사용
when(someMock.someMethod("foo", "hello")).thenReturn(something);
👇 테스트 예제
@Test
void 회원_생성_성공() {
// given
// 가상의 로그인 ID를 지정
String loginId = "kqk1234";
// [1] memberRepository.findByLoginId(loginId) 호출 시 Optional.empty()를 리턴하도록 설정
// → 이미 존재하는 회원이 없는 상태라고 가정
when(memberRepository.findByLoginId(loginId)).thenReturn(Optional.empty());
// [2] memberRepository.save(...) 메서드가 호출되면, 전달된 인자(Member 객체)를 그대로 리턴
// → 실제 저장소가 없으므로, 저장된 결과처럼 동작하게 만듦
// `any()`는 어떤 Member 객체든 상관없이 매칭되게 함 (모든 객체 허용하는 매처)
when(memberRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
// [3] 테스트용 회원 생성 요청 DTO 준비
MemberCreateDto dto = new MemberCreateDto(
loginId,
"dqwjfir!", // 비밀번호
"testMember", // 이름
LocalDate.now(), // 생년월일
"xuni1234@gmail.com", // 이메일
"010-1111-1111", // 연락처
EnterStatus.ENTER // 입장 상태
);
// when
// [4] 테스트 대상인 memberService.joinMember() 호출
// → 내부에서 memberRepository.findByLoginId(), save() 등이 호출됨
memberService.joinMember(dto);
// then
// [5] save()에 전달된 Member 객체를 캡처해서 검증
// → 진짜 DB에 저장되진 않지만, 어떤 객체가 저장되었는지 확인 가능
ArgumentCaptor<Member> captor = ArgumentCaptor.forClass(Member.class);
// [6] save()가 호출되었고, 그때 어떤 Member가 들어갔는지 캡처함
verify(memberRepository).save(captor.capture());
// [7] 캡처된 Member 객체를 꺼냄
Member savedMember = captor.getValue();
// [8] 저장된 Member의 필드 값이 예상한 값인지 검증
assertEquals("kqk1234", savedMember.getLoginId());
assertEquals("testMember", savedMember.getUsername());
// [9] save() 메서드가 실제로 호출되었는지도 한 번 더 검증
verify(memberRepository).save(any());
}
'Spring' 카테고리의 다른 글
[Spring] 프로파일(profiles) 로딩 우선순위 (1) | 2025.05.29 |
---|---|
[Spring] BCryptPasswordEncoder (0) | 2025.04.17 |
[Spring] 스프링 빈과 스프링 컨테이너 (0) | 2025.01.12 |
[Spring] 예외 처리 방식 (0) | 2024.12.11 |
[Spring] Security JWT (0) | 2024.11.10 |