본문 바로가기
자바

Mockito 사용해보기

by jeonghaemin 2022. 4. 17.
728x90

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);

참고

댓글