이번 포스팅에서는 Mockito를 활용해 Mock 객체를 만들고, 원하는 동작을 지정(Stubbing)하고, 메서드 호출 여부나 호출 순서를 검증하는 방법에 대해 정리해보려고 한다. 예제 코드는 JUnit5와 Mockito를 사용하고 있으며, @ExtendWith(MockitoExtension.class)를 통해 Mockito 관련 설정을 편리하게 적용했다.
Mockito 기본 개념
Mock 객체란?
실제 객체처럼 동작하지만, 프로그래머가 원하는 대로 그 객체의 행위를 설정(Stubbing)할 수 있는 가짜 객체다. Mockito를 사용하면 아주 간단히 Mock 객체를 만들고 관리할 수 있다. Mock 객체를 통해 외부 의존성을 가진 코드(예: DB 접근, 외부 API 호출)를 테스트 시점에 격리시키고, 원하는 시나리오에 맞춰 동작을 흉내낼 수 있다.
테스트 예제 살펴보기
아래는 StudyServiceTest 클래스의 일부 코드다. 여기서는 MemberService, StudyRepository를 Mock으로 만들어, StudyService를 테스트하고 있다. MemberService나 StudyRepository는 실제 DB 접근이나 외부 연동을 하는 서비스라고 가정하고, 테스트에서는 이 로직을 격리하기 위해 Mock을 사용한다.
@ExtendWith(MockitoExtension.class)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyServiceTest {
@Mock MemberService memberService;
@Mock StudyRepository studyRepository;
Member member;
Study study;
@BeforeEach
void set_up() {
member = Member.builder().id(1L).email("test@test.com").build();
study = new Study(10);
}
@Test
void any_long_test() {
// memberService.findById(anyLong()) 호출 시 member를 감싼 Optional 리턴하기
when(memberService.findById(anyLong())).thenReturn(Optional.of(member));
Member byId1 = memberService.findById(1L)
.orElseThrow(MemberNotFoundException::new);
Member byId2 = memberService.findById(2L)
.orElseThrow(MemberNotFoundException::new);
// 두 번의 호출 모두 같은 member를 반환하는지 검증
assertEquals(member, byId1);
assertEquals(member, byId2);
}
...
}
Mock Stubbing
when(memberService.findById(anyLong())).thenReturn(Optional.of(member));는 흔히 말하는 Stubbing이다. "어떤 메서드가 호출될 때 어떤 값을 리턴하거나 예외를 던질지"를 정하는 것. Stubbing을 하지 않으면 Mock 객체의 기본 동작은 다음과 같다:
- 모든 객체 리턴 타입: null
- Optional 타입: Optional.empty()
- primitive 타입: 기본값(0, false 등)
- 컬렉션: 비어있는 컬렉션
- Void 메서드: 아무 일도 일어나지 않음
Stubbing을 통해 findById(anyLong()) 호출 시 Optional.of(member)를 반환하도록 설정했기 때문에, 이후 호출에서도 언제나 이 값이 반환된다.
@Test
void do_throw_test() {
// 특정 상황에서 예외 던지기
doThrow(new IllegalArgumentException()).when(memberService).validate(1L);
assertThrows(IllegalArgumentException.class, () -> memberService.validate(1L));
// 2L 호출시에는 예외가 발생하지 않음
memberService.validate(2L);
}
이 예제에서는 void 타입 메서드를 대상으로 예외를 던지도록 설정했다. doThrow(...).when(...) 패턴을 사용해 특정 인자에 대해 호출할 때 예외를 발생시킬 수 있다.
@Test
void what_the_test() {
when(memberService.findById(any()))
.thenReturn(Optional.of(member)) // 첫 번째 호출
.thenThrow(MemberNotFoundException.class) // 두 번째 호출
.thenReturn(Optional.empty()); // 세 번째 호출
// 1차 호출: member 반환
assertEquals(member, memberService.findById(1L).orElseThrow(MemberNotFoundException::new));
// 2차 호출: 예외 발생
assertThrows(MemberNotFoundException.class, () -> {
memberService.findById(2L).orElseThrow(MemberNotFoundException::new);
});
// 3차 호출: Optional.empty()
assertEquals(Optional.empty(), memberService.findById(3L));
}
thenReturn()이나 thenThrow()는 이렇게 체이닝할 수 있어서, 같은 메서드가 여러 번 호출될 때 각각 다른 결과를 내려줄 수 있다.
Mock 객체 검증(Verify)
@Test
void mock_verify_test() {
// given
StudyService studyService = new StudyService(memberService, studyRepository);
when(memberService.findById(1L)).thenReturn(Optional.of(member));
when(studyRepository.save(study)).thenReturn(study);
Member byId = memberService.findById(1L)
.orElseThrow(MemberNotFoundException::new);
// when
studyService.createNewStudy(byId.getId(), study);
// then
// 특정 메서드가 몇 번 호출되었는지, 전혀 호출되지 않았는지 검증
verify(memberService, times(1)).notify(study);
verify(memberService, times(1)).notify(member);
verify(memberService, never()).validate(any());
}
verify()를 사용하면 Mock 메서드가 기대한 만큼 호출됐는지 검증할 수 있다.
- times(1) : 정확히 한 번 호출이어야 함
- never() : 호출되지 않아야 함
호출 순서를 검증하는 것도 가능하다.
InOrder inOrder = inOrder(memberService);
inOrder.verify(memberService).notify(study); // 먼저 study로 notify
inOrder.verify(memberService).notify(member); // 그 다음 member로 notify
이렇게 하면 notify(study)가 먼저 호출되고, 그 다음에 notify(member)가 호출되었는지 확인할 수 있다.
BDD 스타일로 Mockito 사용하기
Mockito는 BDD(Behavior-Driven Development) 스타일을 지원한다. when -> given, thenReturn -> willReturn, verify -> then().should() 형태로 좀 더 자연스럽게 읽히는 코드를 작성할 수 있다.
@Test
void BDD_test() {
// given
StudyService studyService = new StudyService(memberService, studyRepository);
given(memberService.findById(1L)).willReturn(Optional.of(member));
given(studyRepository.save(study)).willReturn(study);
Member byId = memberService.findById(1L)
.orElseThrow(MemberNotFoundException::new);
// when
studyService.createNewStudy(byId.getId(), study);
// then
then(memberService).should(times(1)).notify(study);
then(memberService).should(times(1)).notify(member);
// notify 호출 순서나 횟수등도 동일하게 검증 가능
}
이렇게 하면 읽기도 좋고, 요구사항을 코드로 표현하기 수월해진다.
정리
Mockito를 사용하면 다음과 같은 이점이 있다.
- Mock 객체 생성: @Mock 어노테이션과 @ExtendWith(MockitoExtension.class)로 간단히 Mock 객체를 주입받을 수 있다.
- Stubbing: when() / given()을 통해 특정 메서드 호출 시 반환값 혹은 예외를 설정할 수 있다.
- Verify: verify() / then().should()를 통해 메서드 호출 여부, 횟수, 순서를 검증할 수 있다.
이를 통해 DB나 외부 서비스에 의존하지 않고, 순수한 비즈니스 로직 단위 테스트를 편리하게 작성할 수 있다. 이렇게 격리된 단위 테스트는 빠르고 신뢰성 있으며, 리팩토링 과정에서도 안전하게 코드 품질을 유지하는 데 도움이 된다.
목키토.. 뭔가 이름이 열받아용
'코딩딩 > Spring' 카테고리의 다른 글
TDD와 BDD, 그리고 테스트 작성의 기본 개념들 (3) | 2024.12.11 |
---|---|
테스트 코드, 어디까지 작성해야 할까? (5) | 2024.12.11 |
JUnit5 정리 (1) | 2024.12.11 |
th:object (0) | 2024.01.18 |
Entity의 Id값을 Long으로 주는 이유가 뭘까? (0) | 2024.01.02 |