Java - Mockito를 이용하여 테스트 코드 작성하는 방법

JS · 27 Sep 2020

Mockito는 Java에서 인기있는 Mocking framework입니다. Mockito로 객체를 mocking하여 Unit Test를 작성할 수 있습니다.

직접 Mock 객체를 만들 수 있지만 Mockito와 같은 Mocking framework을 사용하면 번거로운 코드를 작성하지 않아도 됩니다.

이 글에서는 Mockito로 어떻게 테스트 코드를 작성하는지 알아보겠습니다.

  • Mocking
  • Verify
  • ArgumentCaptor
  • Spying
  • Exception 발생

의존성 설정

gradle 프로젝트에서 다음과 같이 의존성을 설정하면 JUnit과 Mockito 라이브러리를 사용할 수 있습니다.

dependencies {
    testImplementation 'junit:junit:4.12'
    testImplementation 'org.mockito:mockito-core:2.7.22'
}

Mocking

다음과 같이 mock(ClassToMock.class)으로 Mock 객체를 만들 수 있습니다.

Mock 객체를 만들면 내부의 코드는 모두 동작하지 않고, 리턴 값은 0, false, null 등으로 대체됩니다.

@Test
public void mockingList() {
    List mockList = mock(ArrayList.class);

    mockList.add("apple");
    assertEquals("apple", mockList.get(0));
}

위 코드를 실행해보면 null이 리턴되어 테스트가 실패합니다.

java.lang.AssertionError:
Expected :apple
Actual   :null

When

when을 사용하면 어떤 상황에서 Mock 객체가 어떤 값을 리턴하도록 만들 수 있습니다. 이것을 Stubbing이라고 합니다.

다음 코드를 해석하면 mockList객체가 get(0)을 호출할 때, "appple"을 리턴하라는 것입니다.

when(mockList.get(0)).thenReturn("apple");

다음과 같이 get(), size()가 호출될 때 특정 값이 설정되도록 하였습니다.

@Test
public void mockingList_when() {
    List mockList = mock(ArrayList.class);

    when(mockList.get(0)).thenReturn("apple");
    when(mockList.get(1)).thenReturn("kiwi");
    when(mockList.size()).thenReturn(10);

    assertEquals("apple", mockList.get(0));
    assertEquals("kiwi", mockList.get(1));
    assertEquals(10, mockList.size());
}

입력되는 인자와 무관하게 특정 값을 리턴하고 싶다면 anyInt()를 사용하면 됩니다. 이 외에 anyString(), anyLong() 등 모든 타입에 대한 메소드들이 제공됩니다.

@Test
public void mockingList_when2() {
    List mockList = mock(ArrayList.class);

    when(mockList.get(anyInt())).thenReturn("apple");
    assertEquals("apple", mockList.get(0));
    assertEquals("apple", mockList.get(1));
}

Verify

verify를 이용하면 mock 객체에 어떤 API가 호출되었는지, 몇번 호출되었는지 확인할 수 있습니다.

다음 코드는 mockList.add("apple")이 호출되었는지 확인합니다. 만약 호출되지 않았다면 테스트는 실패합니다.

verify(mockList).add("apple");

다음 코드는 mockList.get(0)이 두번 호출되었는지 확인합니다. times()의 인자에 예상되는 호출 횟수를 전달합니다.

verify(mockList, times(2)).get(0);

그래서 다음과 같이 메소드가 호출되었는지 확인할 수 있습니다. atLeastOnce(), atLeast(), times() 등의 메소드를 제공해주어 몇번 호출되었는지도 확인할 수 있습니다.

@Test
public void mockingList_verify() {
    List mockList = mock(ArrayList.class);

    mockList.add("apple");
    verify(mockList).add("apple");

    mockList.get(0);
    mockList.get(0);
    verify(mockList, times(2)).get(0);
    verify(mockList, atLeast(2)).get(0);
}

ArgumentCaptor

ArgumentCaptor는 Mock 객체에 전달된 인자를 확인하는 용도로 사용합니다.

다음 코드는 mockList.add()의 인자를 읽어오는 코드입니다. verify에 capture()를 전달하고 getValue()로 어떤 값이 전달되었는지 확인할 수 있습니다.

ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);
verify(mockList).add(arg.capture());
assertEquals("apple", arg.getValue());

다음과 같이 ArgumentCaptor를 이용하여 add()에 어떤 인자가 전달되었는지 확인할 수 있습니다.

@Test
public void captorExample() {
    ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);

    mockList.add("apple");
    verify(mockList).add(arg.capture());

    assertEquals("apple", arg.getValue());
}

Spying

Mock 객체는 내부 구현이 동작하지 않고 0, false, null 등이 리턴된다고 했는데요. Spy 객체는 실제 객체처럼 동작하고, verify로 어떤 메소드가 호출되었는지 확인할 수 있습니다. 또한, when 키워드로 일부 메소드를 stubbing할 수 있습니다.

다음과 같이 spy 객체를 만들 수 있습니다. 실행해보면 테스트는 pass되며 실제 객체처럼 동작한다는 것을 알 수 있습니다.

@Test
public void spyingList() {
    List spyList = spy(ArrayList.class);

    spyList.add("apple");
    assertEquals("apple", spyList.get(0));
}

다음과 같이 verify로 메소드가 호출되었는지 검증할 수 있습니다.

@Test
public void spyingList_verify() {
    List spyList = spy(ArrayList.class);

    spyList.add("apple");
    verify(spyList).add("apple");

    spyList.get(0);
    verify(spyList).get(0);
}

또한, 다음과 같이 when으로 일부 메소드를 stubbing할 수 있습니다. 하지만 아래 코드는 문제가 있습니다.

@Test
public void spyingList_when_error() {
    List spyList = spy(ArrayList.class);

    when(spyList.get(0)).thenReturn("apple");
    when(spyList.get(1)).thenReturn("kiwi");
    when(spyList.size()).thenReturn(10);

    assertEquals("apple", spyList.get(0));
    assertEquals("kiwi", spyList.get(1));
    assertEquals(10, spyList.size());
}

위 코드를 실행하면 IndexOutOfBoundsException예외가 발생하면서 테스트는 실패합니다. spy 객체는 실제 객체처럼 동작하기 때문에 when 코드에서 spyList.get(0)을 호출할 때 Item이 없어 Exception이 발생할 수 있습니다.

 java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

 	at java.util.ArrayList.rangeCheck(ArrayList.java:659)
 	at java.util.ArrayList.get(ArrayList.java:435)

이럴 때는 doReturn().when() 패턴으로 변경해주면 성공적으로 stubbing할 수 있습니다.

@Test
public void spyingList_when_ok() {
    List spyList = spy(ArrayList.class);

    doReturn("apple").when(spyList).get(0);
    doReturn("kiwi").when(spyList).get(1);
    doReturn(10).when(spyList).size();

    assertEquals("apple", spyList.get(0));
    assertEquals("kiwi", spyList.get(1));
    assertEquals(10, spyList.size());
}

따라서, spy 객체를 stubbing할 때는 when().thenReturn() 패턴을 사용하지 말고 doReturn().when() 패턴을 사용하는 것이 좋습니다.

Exception 발생

특정 상황에서 Exception이 발생하도록 할 수도 있습니다. when().thenThrow(ExceptionClass) 패턴을 사용하면 됩니다.

@Test
public void whenGetUserInfo_throwException() {
    Contacts contactsMock= mock(Contacts.class);
    when(contactsMock.getUserInfo(anyInt()))
            .thenThrow(NullPointerException.class);

    contactsMock.getUserInfo(0);
}

thenThrow()에 인자로 클래스 이름을 전달하지 않고 new NullPointerException(msg)처럼 Exception 객체를 생성하여 전달할 수도 있습니다.

@Test
public void whenGetUserInfo_throwExceptionWithMsg() {
    Contacts contactsMock= mock(Contacts.class);
    when(contactsMock.getUserInfo(anyInt()))
            .thenThrow(new NullPointerException("Null pointer exception happened"));

    contactsMock.getUserInfo(0);
}

위의 코드들을 실행하면 Exception이 발생하며 테스트는 실패합니다. Exception이 발생되는 것을 확인한 것이기 때문에 테스트는 패스되어야 합니다.

다음과 같이 NullPointerException이 발생하지 않으면 테스트는 실패하고, Exception이 발생하면 성공하도록 구현할 수 있습니다.

@Test
public void whenGetUserInfo_throwException2() {
    Contacts contactsMock= mock(Contacts.class);
    when(contactsMock.getUserInfo(anyInt()))
            .thenThrow(NullPointerException.class);

    try {
        contactsMock.getUserInfo(0);
        fail();
    } catch (NullPointerException e) {
        // pass
    }
}

더 간단하고 가독성이 좋은 방법은 @Test에 다음과 같이 예외가 발생할 것이라고 명시하는 것입니다. 만약 Exception이 발생하면 테스트는 성공하며, 그렇지 않으면 테스트는 실패합니다.

@Test(expected = NullPointerException.class)
public void whenGetUserInfo_throwException3() {
    Contacts contactsMock= mock(Contacts.class);
    when(contactsMock.getUserInfo(anyInt()))
            .thenThrow(NullPointerException.class);

    contactsMock.getUserInfo(0);
}

Void-Return Type 메소드에서 예외 발생

Void로 설정된 메소드는 다음과 같이 doThrow().when() 패턴으로 Exception이 발생하도록 구현해야 합니다.

@Test(expected = NullPointerException.class)
public void whenCallsVoidReturnTypeMethod_throwException() {
    Contacts contactsMock= mock(Contacts.class);
    doThrow(NullPointerException.class)
            .when(contactsMock)
            .addUserInfo(anyString(), anyString());

    contactsMock.addUserInfo("AAA", "123-456");
}

spy 객체에서 예외 발생

다음과 같이 Spy 객체도 Mock과 동일한 방식으로 Exception을 발생시킬 수 있습니다.

@Test(expected = NullPointerException.class)
public void whenGetUserInfo_throwException4() {
    Contacts contactsMock= spy(Contacts.class);
    when(contactsMock.getUserInfo(anyInt()))
            .thenThrow(NullPointerException.class);

    contactsMock.getUserInfo(0);
}

정리

Mockito 라이브러리를 사용하여 Mock과 Spy 객체를 만들고 verify로 검증하는 방법을 알아보았습니다. 또한, when으로 stubbing하여 특정 값을 리턴하거나 예외를 발생시키도록 만드는 방법을 알아보았습니다.

댓글을 보거나 쓰려면 이 버튼을 눌러주세요.
codechachaCopyright ©2019 codechacha