Mockito - static, final method를 mocking하는 방법

JS · 29 Jan 2020

Mockito는 final과 static method를 mocking, spying하는 것을 지원하지 않습니다. 하지만, Dexmaker의 Mockito 라이브러리를 이용하면 안드로이드에서 final, static method를 mocking, spying 할 수 있습니다.

안드로이드 프로젝트에서 Dexmaker mockito를 설정하고, final, static method를 mocking하는 예제를 소개합니다.

Mockito를 이용하여 테스트 코드를 작성하는 기본적인 방법은 Android Mockito로 테스트 코드 작성하기를 참고해주세요.

프로젝트 설정

Dexmaker는 P OS 이상의 SDK(API 28)만 사용할 수 있습니다. P 이전 버전은 사용할 수 없습니다.

프로젝트를 만들면 App의 build.gradle에 다음과 같이 Java 8을 지원하도록 합니다.

android {
  ...
  compileOptions {
      targetCompatibility JavaVersion.VERSION_1_8
      sourceCompatibility JavaVersion.VERSION_1_8
  }
  ...
}

그리고 build.gradle의 의존성에 다음과 같이 라이브러리를 추가합니다. dexmaker는 내부적으로 org.mockito:mockito-core를 참조하기 때문에 다른 라이브러리는 별도로 추가하지 않아도 됩니다.

dependencies {
    ...
    androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito-inline-extended:2.25.1'

}

Mocking final method

final method를 mocking하는 예제입니다. 기존에 Mockito에서 non-final 메소드를 mocking하는 방법과 동일합니다.

class FinalTrojan {
    final String finalOpen() { return "horse"; }
}

@Test
public void testStubbingFinalMethod() {
    FinalTrojan mockF = mock(FinalTrojan.class);
    when(mockF.finalOpen()).thenReturn("soldiers");

    assertEquals("soldiers", mockF.finalOpen());
}

Mocking static method

staic method를 mocking하는 예제입니다.

private static class SuperClass {
    final String returnA() {
        return "superA";
    }

    static String returnB() {
        return "superB";
    }

    static String returnC() {
        return "superC";
    }
}

@Test
public void mockingExample() {
    assertEquals("superB", SuperClass.returnB()); // 1

    MockitoSession session = mockitoSession()
        .mockStatic(SuperClass.class).startMocking(); // 2
    try{
        assertNull(SuperClass.returnB()); // 3

        when(SubClass.returnB()).thenReturn("fakeB");  // 4
        assertEquals("fakeB", SuperClass.returnB());  // 5
    } finally {
        session.finishMocking();  // 6
    }
}
  1. mocking하지 않으면 실제 클래스에서 구현된 값이 리턴됩니다.
  2. MockitoSession 객체로 클래스를 mocking합니다.
  3. mocking이 시작되면, 메소드는 null을 리턴합니다.
  4. when, thenReturn 을 이용하여 mocking을 구현합니다.
  5. 메소드를 호출했을 때, 설정한 값이 리턴되는지 확인합니다.
  6. mocking이 끝나면 finishMocking()를 호출하여 종료해 줍니다.

mocking은 mockStatic(SuperClass.class) 처럼, spying은 spyStatic(SuperClass.class) 처럼 사용하면 됩니다.

Mocking Subclass

Subclass를 mocking하는 예제입니다.

private static class SuperClass {
    final String returnA() {
        return "superA";
    }

    static String returnB() {
        return "superB";
    }

    static String returnC() {
        return "superC";
    }
}

private static final class SubClass extends SuperClass {
    static String recorded = null;

    static String returnC() {
        return "subC";
    }

    static final String record(String toRecord) {
        recorded = toRecord;
        return "record";
    }
}

@Test
public void mockOverriddenStaticMethod() throws Exception {
    MockitoSession session = mockitoSession().mockStatic(SubClass.class).startMocking();
    try {
        // By default all static methods of the mocked class should return the default answers
        assertNull(SubClass.returnB());
        assertNull(SubClass.returnC());

        // Super class is not mocked
        assertEquals("superB", SuperClass.returnB());
        assertEquals("superC", SuperClass.returnC());

        when(SubClass.returnB()).thenReturn("fakeB");
        when(SubClass.returnC()).thenReturn("fakeC");

        // Make sure behavior is changed
        assertEquals("fakeB", SubClass.returnB());
        assertEquals("fakeC", SubClass.returnC());

        // Super class should not be affected
        assertEquals("superB", SuperClass.returnB());
        assertEquals("superC", SuperClass.returnC());
    } finally {
        session.finishMocking();
    }

    // Mocking should be stopped
    assertEquals("superB", SubClass.returnB());
    assertEquals("subC", SubClass.returnC());
}

인자가 있는 static method mocking

인자가 있는 static method를 mocking하는 예제입니다.

static class S {
    static String staticEcho(String in) {
        return in;
    }
}

@Test
public void testStubbingStaticMethod() {
    MockitoSession session = mockitoSession().mockStatic(S.class).startMocking();
    try {
        when(S.staticEcho("Marco")).thenReturn("Polo");
        assertEquals("Polo", S.staticEcho("Marco"));
    } finally {
        session.finishMocking();
    }

    // Once the session is finished, all stubbings are reset
    assertEquals("Marco", S.staticEcho("Marco"));
}

Verify static method

static method를 verify하는 방법입니다.

여기서는 mocking이 아니라 spying을 하였습니다. mocking은 클래스 내부에 구현된 메소드를 stub으로 만들지만, spying은 구현된 코드를 그대로 사용한다는 차이점이 있습니다.

verify는 지정한 메소드가 호출되었는지, 몇번 호출되었는지 확인하는 방법입니다. 행위(Behavior) 관점에서 테스트를 수행할 수 있습니다.

verify()는 예상되는 횟수와 실제 호출되는 횟수가 다른 경우 테스트를 실패로 처리합니다.

다음은 UtilClass 클래스를 spying하고, static 메소드가 몇번 호출되는지 verify하는 코드입니다.

static class UtilClass {
    public static String staticMethod(String str) {
        return str;
    }

    public static void staticVoidMethod(String str) {
        staticMethod(str);
    }
}

@Test
public void testVerifyStaticMethod() {
    MockitoSession session =
        mockitoSession().spyStatic(UtilClass.class).startMocking();
    try {
        UtilClass.staticVoidMethod("string");
        UtilClass.staticMethod("string");

        verify(() -> UtilClass.staticMethod("string"), atLeastOnce());
        verify(() -> UtilClass.staticMethod("string"), times(2));
        verify(() -> UtilClass.staticVoidMethod("string"), atLeastOnce());
        verify(() -> UtilClass.staticVoidMethod("string"), atMost(2));
    } finally {
        session.finishMocking();
    }
}

verify는 () -> UtilClass.staticMethod()와 같이 람다표현식으로 호출할 메소드를 전달합니다. 그리고 atLeastOnce()와 같은 횟수 정보를 인자로 전달합니다.

아래 API들은 예상되는 메소드의 호출 횟수를 정의하며, 의미는 다음과 같습니다.

  • atLeastOnce(): 메소드가 최소 한번 호출되었는지 확인
  • times(wanted number): 원하는 수만큼 호출되었는지 확인
  • atMost(max number): 최대 호출 횟수, 초과되면 테스트 실패
  • atLeast(min number): 최소 호출 횟수, 미만이면 테스트 실패

Reset mock

Mocking한 것을 reset() API로 초기화하는 예제입니다.

@Test
public void resetMock() throws Exception {
    MockitoSession session =
        mockitoSession().mockStatic(SuperClass.class).startMocking();
    try {
        assertNull(SuperClass.returnB());

        when(SuperClass.returnB()).thenReturn("fakeB");
        assertEquals("fakeB", SuperClass.returnB());

        reset(staticMockMarker(SuperClass.class));
        assertNull(SuperClass.returnB());
    } finally {
        session.finishMocking();
    }
}

안드로이드 메소드에 적용

안드로이드의 Settings의 메소드를 spying하는 예제입니다.

@Test
public void spyStatic() throws Exception {
    ContentResolver resolver =
        InstrumentationRegistry.getTargetContext().getContentResolver();
    String deviceName = Settings.Global.getString(resolver, DEVICE_NAME);

    MockitoSession session =
        mockitoSession().spyStatic(Settings.Global.class).startMocking();
    try {
        // Cannot call when(Settings.getString(any(ContentResolver.class), eq("...")))
        // as any(ContentResolver.class) returns null which makes getString fail. Hence need to
        // use less lambda API
        doReturn("23").when(() -> Settings.Global.getString(any
                (ContentResolver.class), eq("twenty three")));

        doReturn(42).when(() -> Settings.Global.getInt(any
                (ContentResolver.class), eq("fourty two")));

        // Make sure behavior is changed
        assertEquals("23", Settings.Global.getString(resolver, "twenty three"));
        assertEquals(42, Settings.Global.getInt(resolver, "fourty two"));

        // Make sure non-mocked methods work as before
        assertEquals(deviceName, Settings.Global.getString(resolver, DEVICE_NAME));
    } finally {
        session.finishMocking();
    }
}

샘플

이 글에서 사용한 샘플은 GitHub - Sample에서 확인할 수 있습니다.

참고

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