Mockito 사용해보기
Mockito는 자바에서 단위 테스트 코드를 작성할 때 많이 사용되는 Mock 프레임워크 중 하나입니다.
Mock이란 진짜 객체와 비슷하지만, 프로그래머가 그 객체의 행동을 관리하는 가짜 객체를 말합니다. Mock을 사용하여 테스트 코드를 작성하면 객체가 어떤 행동을 할 때 프로그래머가 의도한 결과를 반환하도록 정의하여 사용할 수 있습니다.
그렇다면 Mock은 어떤 경우에 사용할까요? 만약 단위 테스트 코드를 작성하는데 애플리케이션에서 데이터베이스나 외부 API를 사용하는 경우, API 호출과 Repository 객체를 Mock으로 만들어 사용하면 외부 환경에 영향을 받지 않고 테스트를 수행할 수 있게 됩니다.
Mockito 의존성 추가하기
스프링부트를 사용하는 경우 spring-boot-starter-test
를 추가하면 Mockito가 포함되어 있습니다.
스프링부트를 사용하지 않는다면 아래 두 의존성을 추가해주어야 합니다.
예시 상황
아래 코드와 같이 MemberRepository가 인터페이스로만 정의되어 있는 상황에서, MemberRepository를 Mock 객체로 만들어 사용해 보겠습니다.
@Repository
public interface MemberRepository {
void save();
Member findById(Long id);
}
Mock 객체 생성
MemberRepsitory Mock 객체를 만들어보도록 하겠습니다. Mock 객체는 크게 2가지 방법으로 생성할 수 있습니다.
1. Mockito.mock() 메서드 사용하여 생성하기
@Test
void registerMemberTest() {
MemberRepository memberRepository = Mockito.mock(MemberRepository.class);
}
2. @Mock 애노테이션을 사용하여 생성하기
- @Mock 애노테이션을 사용하는 경우 메서드 파라미터 또는 필드로 주입받아 사용할 수 있습니다.
- @Mock 애노테이션을 통해 생성하는 경우 클래스에
@ExtendWith(MockitoExtension.class)
을 적어줘야 합니다.
// 필드 주입
@ExtendWith(MockitoExtension.class)
class MemberServiceTest {
@Mock MemberRepository memberRepository;
// ..
}
// 메서드 파라미터 주입
@ExtendWith(MockitoExtension.class)
class MemberServiceTest {
@Test
void registerMemberTest(@Mock MemberRepository memberRepository) {
// ..
}
}
Mock 객체 Stubbing
Mock 객체를 생성했으면 다음으로는 목 객체의 행동을 지정해 주어야 합니다.
아무 행동도 지정해 주지 않은 Mock 객체는 기본적으로 다음과 같이 동작합니다.
- null을 반환한다.(반환 타입이 Optional인 경우 Optional.empty() 반환)
- 프리미티브 타입인 경우 각 타입의 기본 값을 반환한다.
- 컬렉션은 비어있는 컬렉션을 반환한다.
- 반환 타입이 void 인 경우 아무 일도 발생하지 않는다. 예외도 던지지 않음
Mock 객체의 메서드를 호출할 때 특정 파라미터를 전달하는 경우 어떤 결과가 나와야 하는지, 어떤 예외를 던져줘야 하는지 등을 지정할 수 있습니다.
@Test
void registerMemberTest() {
MemberService memberService = new MemberService(memberRepository);
Member member = new Member();
member.setId(1L);
member.setName("Steve");
member.setEmail("steve@gmail.com");
// memberRespository.findById(1L)의 반환 값은 member 객체
Mockito.when(memberRepository.findById(1L)).thenReturn(member);
// memberRespository.findById(2L)를 호출하면 RuntimeException 발생
Mockito.when(memberRepository.findById(2L)).thenThrow(new RuntimeException());
}
Mockito.when() 파라미터로 메서드를 전달하고, thenReturn() 메서드를 통해 해당 메서드가 실행되었을 때의 결과를 지정하거나 thenThrow()를 사용해서 특정 예외가 발생하도록 지정할
수 있습니다.
void 메서드에서 예외를 던져야 한다면 다음과 같은 형태로 when()
이 아닌 doThrow()
를 사용하는 것이 좋다고 합니다.
(공식 문서에 따르면 컴파일러가 괄호 안의 void 메서드를 선호하지 않기 때문이라고 합니다🤔)
//memberRepository.save(member)는 RuntimeException 발생
doThrow(new RuntimeException()).when(memberRepository).save(member);
assertThrows(RuntimeException.class, () -> {
memberRepository.save(member);
});
또한 ArgumentMatcher라는 것을 사용하여 좀 더 유연하게 메서드의 파라미터를 지정할 수도 있습니다. 예를 들어 any()
를 메서드 인자로 전달하면 어떤 값이 메서드 인자로 들어오든 같은 결과를 반환합니다.
//memberRepository.findById() 메서드에 어떤 인자가 전달되건 member가 반환된다.
Mockito.when(memberRepository.findById(any())).thenReturn(member);
any()
외에도 다양한 ArgumentMatcher 들이 있는데 아래 Mockito 공식 문서를 참고하자
https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html
동일한 인자 같은 메서드를 여러 번 호출하는 경우 메서드 호출 순서에 따라 결과가 다르게 반환되도록 지정할 수도 있습니다.
Mockito.when(memberRepository.findById(any()))
.thenThrow(new RuntimeException()) // 첫번째 호출 시엔 RuntimeException 발생
.thenReturn(member); // 두번째 호출 시엔 member 반환
assertThrows(RuntimeException.class, () -> {
memberRepository.findById(1L);
});
assertEquals(member, memberRepository.findById(1L));
좀 더 간략하게 thenReturn()에 여러 인자를 전달하는 형태로 사용할 수도 있습니다.
Member member1 = new Member();
Member member2 = new Member();
Member member3 = new Member();
/*
memberRepository.findById()
- 첫번째 호출 member1 반환
- 두번째 호출 member2 반환
- 세번째 호출 member3 반환
*/
Mockito.when(memberRepository.findById(any()))
.thenReturn(member1, member2, member3);
assertEquals(member1, memberRepository.findById(1L));
assertEquals(member2, memberRepository.findById(1L));
assertEquals(member3, memberRepository.findById(1L));
Mock 객체의 행동 확인(Verify)
verify()
를 사용해서 Mock 객체의 특정 메서드가 몇 번 호출되었는지, 어떤 순서대로 호출되었는지 등을 확인할 수 있습니다.
메서드 호출 횟수 확인하기
//findById()가 1번 호출되었는지 확인(인자를 전달하지 않으면 기본적으로 1번)
verify(memberRepository).findById(any());
verify(memberRepository, times(1)).findById(any());
//findById()가 3번 호출되었는지 확인
verify(memberRepository, times(3)).findById(any());
//findById()가 호출되지 않았는지 확인
verify(memberRepository, never()).findById(any());
메서드 호출 순서 확인하기
메서드가 어떤 순서대로 호출되었는지 확인할 수도 있습니다.
memberRepository.save(member1);
memberRepository.findById(1L);
memberRepository.save(member2);
InOrder inOrder = inOrder(memberRepository);
inOrder.verify(memberRepository).save(member1);
inOrder.verify(memberRepository).findById(1L);
inOrder.verify(memberRepository).save(member2);
Mock 객체가 호출되지 않았는지 확인하기
//memberRepository mock 객체가 사용되지 않았는지 확인
verifyNoInteractions(memberRepository);
메서드가 특정 시간 안에 호출되었는지 확인하기
//100ms 안에 findById(1L)이 호출되었는지 확인
verify(memberRepository, timeout(100)).findById(1L);
//100ms 안에 findById(2L)이 2번 호출되었는지 확인
verify(memberRepository, timeout(100).times(2)).findById(2L);
Mock 객체가 검증되지 않은 행동을 하는지 확인하기
memberRepository.findById(1L);
memberRepository.findById(2L);
verify(memberRepository).findById(1L);
//memberRepository에 검증되지 않는 호출이 발생했는지 확인
//- memberRepository.findById(2L) 호출은 verify로 검증되지 않는 호출이기 때문에 테스트는 실패한다.
verifyNoMoreInteractions(memberRepository);
BDD 스타일 API
BDDMockito를 통해 Given - When - Then 스타일의 API를 제공해 줍니다.
// Given
Member member = new Member();
// == when(memberRepository.findById(1L)).thenReturn(member)
given(memberRepository.findById(1L)).willReturn(member);
// When
Member findMember = memberRepository.findById(1L);
// Then
// == verify(memberRepository, times(1)).findById(1L)
then(memberService).should(times(1)).findMemberById(1L);
참고
'자바' 카테고리의 다른 글
ObjectMapper에서 LocalDateTime 타입 미지원 에러 해결하기 (0) | 2022.06.02 |
---|---|
일급 컬렉션(First Class Collection)이란? (0) | 2022.05.27 |
[자바] String, StringBuffer, StringBuilder (0) | 2022.03.21 |
djb2 문자열 해시 함수 (0) | 2022.03.04 |
[Java] ThreadLocal (0) | 2021.12.30 |
댓글