-
Notifications
You must be signed in to change notification settings - Fork 28
Mockito
Mock 객체를 쉽게 만들고, 관리하고, 검증할 수 있는 방법을 제공하는 프레임워크
용어 정리
- Mock
- 진짜 객체와 비슷하지만 물리적으로 같지 않고 프로그래머가 직접 행동을 관리하는 객체이다.
- Mock은 모든 상호작용을 기억한다. 사용자는 Mock의 어떤 메서드가 실행되었는지 선택적으로 검증할 수 있다.
- Stubbing
- 테스트 코드에서 Mock 객체를 사용할 때, Mock의 특정 메서드 호출과 응답을 정의하는 것을 말한다.
- Spy
- Mock 객체처럼 모든 상호작용을 기억하면서 객체의 원래 메서드도 호출할 수 있는 객체이다.
테스트를 작성하는 자바 개발자의 약 45%(JetBrains 설문조사)가 Mockito Framework를 사용한다. Mockito를 대체할 수 있는 테스트 프레임워크는 EasyMock, JMock 등이 있다.
- 애플리케이션에서 구현하지 않은 메서드를 테스트하려고 할 때
- 해당 클래스가 어떻게 동작하는지 항상 미리 생각하고 계산하며 테스트를 작성한다면 매우 불편할 것이다.
- 이럴 때 미리 Mock 객체를 생성해서 사용하면 조금 더 편하게 테스트 할 수 있다.
- ex) Member 또는 MemberDao가 어떻게 작동하는지 Mockito를 이용해 만들어 놓으면 Member와 MemberDao 객체를 구현하기 전에도 테스트를 작성할 수 있다.
- 굳이 그럴 필요는 없다.
- 하지만 개발자가 컨트롤하기 힘든 부분이나 아직 구현되지 않은 클래스는 모킹이 필요한 경우가 많다.
- 스프링부트 2.2+ 프로젝트 생성시 spring-boot-start-test에서 자동으로 Mockito를 추가해준다.
- 스프링부트 2.2 버전 이하 또는 스프링부트를 사용하지 않는다면, 아래와 같이 의존성을 추가해준다.
- 자세한 내용은 다음 사이트에서 알아보자
- https://mvnrepository.com/artifact/org.mockito/mockito-core/2.1.0
testImplementation 'org.mockito:mockito-junit-jupiter:5.2.0'
- 테스트하고자 하는 메서드가 의도한 대로 동작하는지 검증하는 것을 말한다. (행위 검증)
@DisplayName("stubbing한 객체의 메서드 호출 횟수를 검증")
@Test
void stubbing_object_method_call_count_validate() {
// given
List<String> mockedList = mock(List.class);
// when
mockedList.add("a");
mockedList.add("a");
// then
verify(mockedList, times(2)).add("a");
}
@DisplayName("stubbing한 객체의 메서드를 한 번도 호출하지 않았는지 검증")
@Test
void stubbing_object_method_never_call() {
// given
List<String> mockedList = mock(List.class);
// when
mockedList.add("a");
mockedList.add("b");
// then
verify(mockedList, never()).get(1);
}
@DisplayName("stubbing한 객체의 메서드가 최소 한 번은 호출되었는지 검증")
@Test
void stubbing_object_method_call_count_at_least_once() {
// given
List<String> mockedList = mock(List.class);
// when
mockedList.add("a");
mockedList.add("b");
// then
verify(mockedList, atLeastOnce()).add("a");
}
@DisplayName("stubbing한 객체의 메서드가 최소 n은 호출되었는지 검증")
@Test
void stubbing_object_method_call_at_least_n() {
// given
List<String> mockedList = mock(List.class);
// when
mockedList.add("a");
mockedList.add("a");
mockedList.add("a");
// then
verify(mockedList, atLeast(3)).add("a");
}
@DisplayName("stubbing한 객체의 메서드가 최대 1번 호출되었는지 검증")
@Test
void stubbing_object_method_call_at_most_once() {
// given
List<String> mockedList = mock(List.class);
// when, then
mockedList.add("a");
verify(mockedList, atMostOnce()).add("a");
}
@DisplayName("stubbing한 객체의 메서드가 최대 1번 호출되었는지 검증")
@Test
void stubbing_object_method_call_at_most_once() {
// given
List<String> mockedList = mock(List.class);
// when, then
mockedList.add("a");
verify(mockedList, atMostOnce()).add("a");
}
@DisplayName("stubbing한 객체의 메서드가 최대 n번 호출되었는지 검증")
@Test
void stubbing_object_method_call_at_most_n() {
// given
List<String> mockedList = mock(List.class);
// when, then
mockedList.add("a");
mockedList.add("a");
mockedList.add("a");
verify(mockedList, atMost(3)).add("a");
}
@DisplayName("inOrder로 stubbing한 객체의 메서드 호출 순서 검증")
@Test
void stubbing_object_method_calls_sequence() {
// given
List<String> mockedList = mock(List.class);
InOrder inOrder = inOrder(mockedList);
// when, then
mockedList.add("a");
mockedList.add("b");
mockedList.add("c");
inOrder.verify(mockedList).add("a");
inOrder.verify(mockedList).add("b");
inOrder.verify(mockedList).add("c");
}
@DisplayName("inOrder로 stubbing한 객체의 메서드 호출 횟수 검증")
@Test
void stubbing_object_method_calls_n() {
// given
List<String> mockedList = mock(List.class);
InOrder inOrder = inOrder(mockedList);
// when, then
mockedList.add("a");
mockedList.add("b");
mockedList.add("c");
inOrder.verify(mockedList, calls(1)).add("a");
inOrder.verify(mockedList, calls(1)).add("b");
inOrder.verify(mockedList, calls(1)).add("c");
}
@DisplayName("해당 검증 메소드만 실행됐는지 검증")
@Test
void stubbing_object_method_call_only() {
// given
List<String> mockedList = mock(List.class);
// when, then
mockedList.add("a");
// mockedList.add("a")와 다른 메서드가 있다면 fail
// mockedList.add("b");
// mockedList.add("c");
verify(mockedList, only()).add("a");
}
@DisplayName("해당 검증 메서드 실행시간 검증")
@Test
void stubbing_object_method_call_time_out() {
// given
List<String> mockedList = mock(List.class);
// when, then
mockedList.add("a");
verify(mockedList, timeout(1)).add("a");
}
When_Then을 이용하여, mock 객체의 메서드를 원하는 방식으로 정의할 수 있다.
- 반환값이 존재하는 메서드를 stubbing
@Test
@DisplayName("반환값이 존재하는 메서드를 stubbing - when_thenReturn")
void whenConfigNonVoidReturnMethodStub1() {
List<Integer> listMock = mock(List.class);
when(listMock.get(anyInt())).thenReturn(STUB_RETURN_VALUE);
final Integer getValue = listMock.get(SECURE_RANDOM.nextInt());
assertThat(getValue)
.isEqualTo(STUB_RETURN_VALUE);
}
위 처럼 when_then을 이용하여, mock 메서드의 반환 값을 간단하게 정의할 수 있다.
- 다른 방식으로 반환값이 존재하는 메서드를 stubbing
@Test
@DisplayName("반환값이 존재하지 않는 메서드를 stubbing - doReturn_when")
void whenConfigNonVoidReturnMethodStub2() {
List<Integer> listMock = mock(List.class);
doReturn(STUB_RETURN_VALUE).when(listMock).get(anyInt());
final Integer getValue = listMock.get(SECURE_RANDOM.nextInt());
assertThat(getValue)
.isEqualTo(STUB_RETURN_VALUE);
}
- 반환값이 존재하지 않는 메서드를 stubbing
/*
void add(int index, E element)
*/
@Test
@DisplayName("반환값이 존재하지 않는 메서드를 stubbing - doNothing_when")
void whenConfigVoidReturnMethodStub() {
ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
List<Integer> listMock = mock(List.class);
doNothing().when(listMock).add(anyInt(), captor.capture());
listMock.add(SECURE_RANDOM.nextInt(), CAPTOR_ARGUMENT_VALUE);
assertThat(captor.getValue())
.isEqualTo(CAPTOR_ARGUMENT_VALUE);
}
- 반환값이 존재하는 메서드에 여러 반환값을 stubbing
@Test
@DisplayName("반환값이 존재하는 메서드에 여러개의 반환값을 stubbing - chaining")
void whenConfigNonVoidReturnMethodStubMultiValue() {
List<Integer> listMock = mock(List.class);
when(listMock.get(anyInt()))
.thenReturn(STUB_RETURN_VALUE)
.thenThrow(CustomException.class);
assertThat(listMock.get(SECURE_RANDOM.nextInt()))
.isEqualTo(STUB_RETURN_VALUE);
assertThatThrownBy(() -> listMock.get(SECURE_RANDOM.nextInt()))
.isInstanceOf(CustomException.class);
}
when에 여러 then… 메서드를 체이닝하여 다양한 값이 반환되도록 설정할 수 있다.
설정한 값들보다 더 많이 메서드를 호출하면, 가장 마지막 값이 반복되어 반환된다.
@Test
@DisplayName(
"반환값이 존재하는 메서드에 여러개의 반환값을" +
"stubbing - chaining 설정값보다 더 많이 호출할 때")
void whenConfigNonVoidReturnMethodStubMultiValue2() {
List<Integer> listMock = mock(List.class);
when(listMock.get(anyInt()))
.thenReturn(STUB_RETURN_VALUE)
.thenThrow(CustomException.class);
assertThat(listMock.get(SECURE_RANDOM.nextInt()))
.isEqualTo(STUB_RETURN_VALUE);
assertThatThrownBy(() -> listMock.get(SECURE_RANDOM.nextInt()))
.isInstanceOf(CustomException.class);
assertThatThrownBy(() -> listMock.get(SECURE_RANDOM.nextInt()))
.isInstanceOf(CustomException.class);
assertThatThrownBy(() -> listMock.get(SECURE_RANDOM.nextInt()))
.isInstanceOf(CustomException.class);
}
- mock객체의 실제 메서드를 호출할 때
@Test
@DisplayName("Mock 객체의 실제 메서드 호출")
void whenConfigCallRealMethod() {
final TestList listMock = mock(TestList.class);
when(listMock.size()).thenCallRealMethod();
assertThat(listMock.size())
.isEqualTo(TestList.CUSTOM_SIZE);
}
static class TestList extends AbstractList<Integer> {
public static final int CUSTOM_SIZE = 20;
@Override
public Integer get(final int index) {
return -1;
}
@Override
public int size() {
return CUSTOM_SIZE;
}
}
- 메서드에 customAnswer를 지정
@Test
@DisplayName("메서드에 Custom Answer 지정")
void whenConfigMethodCustomAnswer() {
final int randomArgument = SECURE_RANDOM.nextInt();
List<Integer> listMock = mock(List.class);
doAnswer(invocation -> invocation.getArgument(0)).when(listMock).get(anyInt());
final Integer returnValue = listMock.get(randomArgument);
assertThat(returnValue)
.isEqualTo(randomArgument);
}
doAnswer를 이용하여, CustomAnswer를 만들어 지정할 수 있다. 함수형 인터페이스 형태기에, 람다로도 작성가능하다.
public interface Answer<T> {
/**
* @param invocation the invocation on the mock.
*
* @return the value to be returned
*
* @throws Throwable the throwable to be thrown
*/
T answer(InvocationOnMock invocation) throws Throwable;
}
Mockito.mock()
을 사용하지 않고 mock 된 인스턴스를 만들 수 있다.
- mock된 인스턴스의 내부는 비어있 다.
- mock 객체를 만드는 방법은 아래와 같다.
@Mock
ArrayList<String> mockedList;
ArrayList<String> mockedList = mock(List.class);
@Mock
ArrayList<String> mockedList;
@DisplayName("목 객체를 스터빙하여 테스트한다. ( @Mock 사용 )")
@Test
public void useMockAnnotation() {
//given
when(mockedList.add("one")).thenReturn(false);
//when
final boolean result = mockedList.add("one");
//then
verify(mockedList).add("one");
Assertions.assertThat(result).isFalse();
}
Mockito.spy()
를 사용하지 않고 기존 인스턴스를 spy
할 수 있다.
-
spy
를 사용한 인스턴스는 기존 인스턴스와 동일하다. - 원하는 부분만
stubbing
할 수 있다. - spy를 객체를 만드는 방법은 아래와 같다.
@Spy
ArrayList<String> spyList;
ArrayList<String> spyList = Mockito.spy(List.class);
@Spy
ArrayList<String> spyList;
@DisplayName("Spy한 객체를 테스트한다. ( @Spy 사용 )")
@Test
public void useMockAnnotation() {
//given
when(spyList.add("one")).thenReturn(false);
//when
final boolean stubResult = spyList.add("one");
final boolean originalResult = spyList.add("two");
//then
//메서드 호출을 검증한다.
verify(spyList).add("one");
verify(spyList).add("two");
Assertions.assertThat(stubResult).isFalse();
Assertions.assertThat(originalResult).isTrue();
}
메서드에 전달된 인자를 캡처하는 기능을 제공한다.
- 캡처할 인자의 타입에 해당하는 Captor 객체를 생성해야 한다.
- verify 메서드로 메서드의 인자를 캡처할 수 있다.
@Captor
final ArgumentCaptor<String> args;
ArgumentCaptor<List> listArgumentCaptor = ArgumentCaptor.forClass(List.class);
@Captor
final ArgumentCaptor<String> args;
@DisplayName("ArgumentCaptor를 사용하여 메서드 호출에 사용된 인자를 저장하여 검증한다.(호출을 한 번 했을 때 )")
@Test
public void useArgumentCaptorOnce() {
// given
final List<String> mockList = mock(List.class);
// when
mockList.add("first");
verify(mockList).add(args.capture());
// then
Assertions.assertThat(args.getValue()).isEqualTo("first");
}
Mock 객체가 주입되어 내부적으로 객체의 메서드가 호출되는 경우에도 인자를 캡쳐할 수 있다.
public class CaptorOuterFixture {
private CaptorInnerFixture captorInnerFixture;
public CaptorOuterFixture(final CaptorInnerFixture captorInnerFixture) {
this.captorInnerFixture = captorInnerFixture;
}
public void show(final String name, final int id, final boolean isExist) {
final CaptorMessageFixture captorMessageFixture = new CaptorMessageFixture(name, id, isExist);
captorInnerFixture.print(captorMessageFixture);
}
}
public class CaptorInnerFixture {
public void print(final CaptorMessageFixture captorMessageFixture) {
System.out.println(captorMessageFixture);
}
}
public class CaptorMessageFixture {
private final String name;
private final int number;
private final boolean isExist;
public CaptorMessageFixture(final String name, final int number, final boolean isExist) {
this.name = name;
this.number = number;
this.isExist = isExist;
}
public String getName() {
return name;
}
}
- Test Code
@DisplayName("내부 동작의 인자 또한 ArgumentCaptor로 캡쳐할 수 있다.")
@Test
void captureInnerParameter() {
//given
final CaptorInnerFixture innerFixture = mock(CaptorInnerFixture.class);
final CaptorOuterFixture captorOuterFixture = new CaptorOuterFixture(innerFixture);
final ArgumentCaptor<CaptorMessageFixture> messageCaptor = ArgumentCaptor.forClass(CaptorMessageFixture.class);
//when
captorOuterFixture.show("메시지", 1, true);
//then
verify(innerFixture).print(messageCaptor.capture());
final CaptorMessageFixture message = messageCaptor.getValue();
Assertions.assertThat(message.getName()).isEqualTo("메시지");
}
해당 객체의 멤버 변수로 존재하는 의존된 다른 객체들이 mock혹은 spy로 생성된 객체라면 의존성 주입을 해주는 기능을 제공한다.
- 간단한 코드로 살펴보자.
- Name이라는 객체를 컴포지션 관계로 가지고 있는 Car 객체다.
public class Car {
private final Name name;
public Car(Name name) {
this.name = name;
}
public boolean checkCar(String name) {
return this.name.isEqualsName(name);
}
}
- field로 가지고 있는 name과 파라미터로 받은 name이 같은지 검증하는 메서드를 가지고 있다.
public class Name {
private final String name;
public Name(String name) {
this.name = name;
}
public boolean isEqualsName(String name) {
return this.name.equals(name);
}
}
- InjectMocks 어노테이션을 사용하지 않는다면 아래와 같이 Mock 객체를 직접 생성자에 넣어 Mock 객체가 주입된 Car 객체를 만들어야 한다.
@Mock
private Name name;
@DisplayName("InjectMocks를 사용하지 않고 Mock 의존성을 주입받는 방법")
@Test
void non_inject_mocks() {
// given
final Car car = new Car(name);
// when
Mockito.when(car.checkCar("hyundai")).thenReturn(true);
// then
Assertions.assertThat(car.checkCar("hyundai")).isTrue();
Assertions.assertThat(car.checkCar("kia")).isFalse();
}
- InjectMocks 어노테이션을 사용한다면 따로 Car를 생성할 때 의존성을 주입하지 않아도 Mock이나 Spy 객체로 생성된 객체가 있다면 의존성을 주입해 준다.
@Mock
private Name name;
@InjectMocks
private Car car;
@DisplayName("InjectMocks를 사용해서 Mock 의존성을 주입받는 방법")
@Test
void inject_mocks() {
// when
Mockito.when(car.checkCar("hyundai")).thenReturn(true);
// then
Assertions.assertThat(car.checkCar("hyundai")).isTrue();
Assertions.assertThat(car.checkCar("kia")).isFalse();
}
-
When(mock.method).ThenThrow(Exception.class)
로 메서드 실행시 Exception이 발생하도록 스터빙 할 수 있다.
@DisplayName("반환값이 존재하는 메서드를 Exception Stubbing 할 때 - class")
@Test
void whenConfigNonVoidReturnMethodToThrowEx_thenExIsThrown() {
List<String> strList = mock(List.class);
when(strList.size()).thenThrow(CustomException.class);
assertThatThrownBy(strList::size)
.isInstanceOf(CustomException.class);
}
-
doThrow(Exception.class).when(mock).method
로 메서드 실행시 Exception이 발생하도록 스터빙할 수 있다.
@DisplayName("반환값이 존재하지 않는 메서드를 Exception Stubbing 할 때 - class")
@Test
void whenConfigVoidReturnMethodToThrowEx_thenExIsThrown() {
List<String> strList = mock(List.class);
doThrow(CustomException.class).when(strList)
.clear();
assertThatThrownBy(strList::clear)
.isInstanceOf(CustomException.class);
}
- 이전에 사용했던
when.thenThrow
나doThrow.when.method
에서 예외 객체 자체를 넘길 수도 있다. - 예외 객체를 생성해 넘김으로써 에러 메시지를 정의해줄 수 있다.
@DisplayName("반환값이 존재하는 메서드를 Exception Stubbing 할 때 - object")
@Test
void whenConfigNonVoidReturnMethodToThrowExWithNewExObj_thenExIsThrown() {
List<String> strList = mock(List.class);
when(strList.size()).thenThrow(new CustomException(EXCEPTION_MESSAGE));
assertThatThrownBy(strList::size)
.isInstanceOf(CustomException.class)
.hasMessage(EXCEPTION_MESSAGE);
}
@DisplayName("반환값이 존재하지 않는 메서드를 Exception Stubbing 할 때 - object")
@Test
void whenConfigVoidReturnMethodToThrowExWithNewExObj_thenExIsThrown() {
List<String> strList = mock(List.class);
doThrow(new CustomException(EXCEPTION_MESSAGE)).when(strList)
.clear();
assertThatThrownBy(strList::clear)
.isInstanceOf(CustomException.class)
.hasMessage(EXCEPTION_MESSAGE);
}
- mocked 메서드를 다양한 방식으로 설정할 수 있다.
- 가장 간단하게 메서드 파라미터에 상수를 넣어 mock 메서드를 설정할 수 있다.
doReturn(STUBED_VALUE).when(mockedList).get(0);
- 하지만 위와 같은 방법은 오로지
mockedList.get(0)
에만 대응하는 방법이다. - 우리는 좀 더 넓은 범위의 파라미터를 정의하거나, 일반적인 상황에 대해 정의할 필요가 있다.
- ArgumentMatcher를 이용하여, 위와같은 문제를 해결할 수가 있다.
doReturn(STUBED_VALUE).when(mockedList).get(anyInt());
- 파라미터 부분에
ArgumentMatchers.anyInt()
를 넣어주면, 모든 Int값이 들어오는 경우에 대하여 Mocked 메서드를 정의할 수 있다. - anyInt외에도한 ArgumentMatchers가 정의 되어있다.
- anyString()
- anyFloat()
- anyList()
와 같은 일반적으로 사용되는 타입 뿐 아니라 any(Class<T> classType)
이라는 메서드로 개인이 정의한 객체 또한 ArgumentMatchers로 이용할 수 있다.
아래는 ArugmentMatchers를 사용한 예시다.
- Mockito는 equals를 사용하여 검증하는데 한 개가 아닌 여러 개의 유연한 검증이 필요할 때 사용된다.
- ArgumentTestFixture
public class ArgumentTestFixture {
public String parseToEnglish(final String korean) {
return null;
}
}
- Test code
@Mock
private ArgumentTestFixture argumentTestFixture;
@DisplayName("인자에 anyString() 을 사용할 경우 어느 string 이 와도 같은 결과로 stubbing 된다.")
@Test
void useArgumentMatcherAnyInt() {
when(argumentTestFixture.parseToEnglish(anyString())).thenReturn("one");
final String oneResult = argumentTestFixture.parseToEnglish(anyString());
final String twoResult = argumentTestFixture.parseToEnglish("일");
final String thirdResult = argumentTestFixture.parseToEnglish("하나");
verify(argumentTestFixture, times(3)).parseToEnglish(anyString());
Assertions.assertThat(oneResult).isEqualTo("one");
Assertions.assertThat(twoResult).isEqualTo("one");
Assertions.assertThat(thirdResult).isEqualTo("one");
}