Android Mockito로 Unit 테스트 코드 작성하기 (kotlin)

JS · 03 Jan 2020

Mockito는 객체를 mocking하는데 사용되는 Java라이브러리입니다. Junit과 함꼐 Unit test를 작성하는데 사용됩니다. Android도 Unit test를 작성하는데 공식적으로 Mockito를 사용하도록 권장하고 있습니다.

Android는 JVM에서 동작하는 Test가 있고, 디바이스 또는 에뮬레이터에서 동작하는 Instrumentation test가 있습니다. Mockito는 두개의 테스트에서 모두 사용할 수 있습니다.

이 글에서는 다음 내용들에 대해서 알아봅니다.

  • Mockito를 사용하여 Unit test를 작성하는 방법
  • Android SDK를 Mocking하여 Test하는 방법

이 글에서 사용되는 코드는 kotlin으로 작성되었습니다.

Gradle 설정

Android Studio 프로젝트의 build.gradle에 다음과 같이 의존성을 설정합니다.

testImplementation 'junit:junit:4.12' // junit
androidTestImplementation("org.mockito:mockito-android:2.24.5")
testImplementation 'org.mockito:mockito-inline:2.21.0'

org.mockito:mockito-core를 사용해도 되지만 kotlin을 사용하는 경우 org.mockito:mockito-inline를 사용하는 것이 좋습니다. (참고: Mockito cannot mock/spy final class error)

testImplementation 'org.mockito:mockito-inline:2.21.0'
// testImplementation 'org.mockito:mockito-core:2.28.2'

그리고, 다음과 같이 returnDefaultValues = true로 설정해 줍니다.

android {
    ...
    testOptions {
        unitTests.returnDefaultValues = true
    }
}

안드로이드 Unit test를 빌드할 때 사용하는 android.jar는 실제 코드가 포함되어있지 않습니다. 위의 설정은, 테스트 코드의 API가 구현되어있지 않을 때 null 또는 0등을 리턴하도록 하여 테스트가 진행되도록 만드는 것입니다. 모든 것을 mocking할 수 없기 때문에 필요한 것만 mocking하고 나머지는 기본 값을 리턴하도록 만드는 것이 편할 수 있습니다. (참고: Andorid Developer)

Mocking 및 Unit test 작성

Unit test를 작성하기 전에 다음과 같이 간단한 클래스를 구현하겠습니다. 이 코드는 앱에서 사용될 코드이기 때문에 Test 폴더에 파일을 만들면 안됩니다.

Example.kt

class Example {

    fun getId() : Int {
        // get id from server and return
        return 0
    }

    fun getUrl(id: Int) : String {
        // get url from server and return
        return ""
    }
}

이제 테스트 코드를 작성할 것입니다.

만약 어떤 객체를 테스트하는데 Example 객체와 의존성이 있을 수 있습니다. Example이 Server에서 데이터를 가져오거나 DB를 사용하는 경우 테스트가 어려울 수 있습니다. 테스트 서버를 만들거나 테스트 DB를 만들어야 할 수 있습니다.

Mockito를 사용하면 Example의 API를 호출할 때 어떤 값을 리턴할지 정할 수 있습니다. 그래서 어떤 값이 항상 리턴된다고 생각하여 의존성을 없앨 수 있습니다. 이렇게 다른 의존성을 제거하고 특정 클래스, Unit만 test하는 코드를 작성할 수 있습니다.

다음과 같이 ../test/아래에 코드를 작성합니다.

import org.junit.Test
import org.junit.Assert.*
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnitRunner

@RunWith(MockitoJUnitRunner::class)   // 1
class ExampleUnitTest {

    @Test   // 2
    fun example1() {
        val example = Mockito.mock(Example::class.java)   // 3

        Mockito.`when`(example.getId()).thenReturn(100)   // 4
        Mockito.`when`(example.getUrl(100))
               .thenReturn("https://codechacha.com")    //  5

        assertEquals(100, example.getId())    // 6
        assertEquals("https://codechacha.com",
            example.getUrl(example.getId()))    // 7
    }
}
  1. Mockito를 사용하려면 이렇게 Annotation을 붙여줍니다. (@RunWith(AndroidJUnit4::class)를 붙여도 동작하긴 하네요.)
  2. Junit은 @Test가 있는 메소드를 테스트 메소드라고 인식합니다.
  3. Example 클래스를 mocking하여 객체로 만들어줍니다.
  4. 이 객체의 getId()가 호출되었을 때 100을 리턴하도록 합니다.
  5. 이 객체의 getUrl(100)가 호출되었을 때 https://codechacha.com를 리턴하도록 합니다. 인자가 꼭 100으로 전달되어야 합니다.
  6. getId()가 100을 리턴하는지 확인합니다.
  7. getUrl(100)이 https://codechacha.com을 리턴하는지 확인합니다.

만약 다음과 같이 org.mockito.Mockito.*으로 import하면 코드가 더 간결해 집니다.

import org.mockito.Mockito.*

val example = mock(Example::class.java)
`when`(example.getId()).thenReturn(100)
`when`(example.getUrl(100)).thenReturn("https://codechacha.com")

anyInt(), anyString()

위에서 Mocking할 때 인자에 정확한 값을 넣어주었습니다. 인자로 어떤 값을 전달하든 상관이 없다면 anyInt(), anyString()등을 사용할 수 있습니다.

다음은 anyInt()를 사용하는 예제입니다. 인자로 어떤 값이 전달되든 리턴 값은 동일합니다.

@Test
fun example2() {
    val example = mock(Example::class.java)

    `when`(example.getId()).thenReturn(100)
    `when`(example.getUrl(anyInt())).thenReturn("https://codechacha.com")

    assertEquals(100, example.getId())
    assertEquals("https://codechacha.com", example.getUrl(0))
}

그 외에 다양한 자료형을 지원하는 메소드들이 있습니다.

  • anyInt()
  • anyString()
  • anyBoolean()
  • anyDouble()
  • anyFloat()
  • anyList()

Exception 발생

어떤 API를 호출했을 때 Exception이 발생되도록 할 수도 있습니다.

다음은 getUrl(20)을 호출할 때 IllegalStateException 예외가 전달되는 예제입니다.

@Test
fun example3() {
    val example = mock(Example::class.java)

    `when`(example.getUrl(anyInt())).thenReturn("https://codechacha.com")
    assertEquals("https://codechacha.com", example.getUrl(10))

    `when`(example.getUrl(20)).thenThrow(IllegalStateException("Exception happened!"))

    try {
        example.getUrl(20)
        fail()
    } catch (e: IllegalStateException) {
        assertEquals(e.message, "Exception happened!")
    }

    doReturn(30).`when`(example).getId()
    assertEquals(30, example.getId())
}

verify()

verify는 어떤 API가 호출되었는지, 몇번 이상 호출되었는지 체크할 때 사용할 수 있습니다. 어떤 메소드를 호출할 때, 그 메소드 안에서 특정 API를 호출했는지 확인하고 싶을 때 사용할 수 있습니다.

다음은 verify를 사용하는 예제입니다.

@Test
fun example4() {
    val example = mock(Example::class.java)
    `when`(example.getId()).thenReturn(100)
    `when`(example.getUrl(100)).thenReturn("https://codechacha.com")

    example.getId()
    example.getId()
    val url = example.getUrl(example.getId())

    verify(example).getUrl(ArgumentMatchers.eq(100))  // 1
    verify(example, times(3)).getId()   // 2
    verify(example, atLeast(2)).getId()   // 3
    verify(example, atLeast(1)).getUrl(100)   // 4
}
  1. getUrl(100)이 호출되었는지 확인합니다.
  2. getId()가 3번 호출되었는지 확인합니다. 2번만 호출되었다면 테스트는 fail됩니다.
  3. getId()가 최소 2번은 호출되었는지 확인합니다.
  4. getUrl(100)이 최소 1번은 호출되었는지 확인합니다.

그 외에 다음 메소드 등을 제공합니다.

  • times()
  • atLeast()
  • atLeastOnce()
  • atMost()

Android SDK를 Mocking 및 Unit test

지금까지 직접 구현한 클래스를 Mocking하여 Unit test를 작성하였습니다. 이제 Android SDK처럼 라이브러리로 제공되는 객체를 Mocking하여 Unit test를 작성해보겠습니다.

먼저 앱에 다음과 같은 코드가 구현되어있다고 가정해보겠습니다. PackageManager를 통해 com.codechacha.sample라는 앱이 설치되어있는지 확인하는 코드입니다.

MainActivity.kt

class MainActivity : AppCompatActivity() {
    companion object {
        const val TAG = "MainActivity"

        fun isSampleAppInstalled(pm: PackageManager): Boolean {
            val SAMPLE_APP_PKG = "com.codechacha.sample"
            val ris = pm.getInstalledPackages(0)
            Log.d(TAG, "ris : $ris")
            for (ri: PackageInfo in ris) {
                if (ri.packageName == SAMPLE_APP_PKG) {
                    return true
                }
            }
            return false
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (isSampleAppInstalled(packageManager)) {
            Log.d(TAG, "Sample app is installed in the device.")
        }
    }
}

위의 코드에서 isSampleAppInstalled()com.codechacha.sample라는 앱이 설치되어있다면 true를 리턴합니다. 테스트하고 싶은 것은 PackageManager로부터 받은 정보에서 sample 앱이 있는지 찾는 코드입니다.

여기서 의존성이 있는 부분은 PackageManager입니다. 디바이스에 설치된 앱 상태에 따라서 결과가 달라지기 때문입니다. 우리는 Mockito를 이용하여 PackageManager가 항상 sample앱이 설치되어있다고 정보를 리턴하도록 만들 수 있습니다.

다음은 위에서 말한 것을 실제로 구현한 코드입니다.

@Test
fun sample_app_is_installed() {
    val pi = PackageInfo()    // 1
    pi.packageName = "com.codechacha.sample"
    val installedApps: List<PackageInfo> = listOf(pi)   // 2

    val pm = Mockito.mock(PackageManager::class.java)  // 3
    Mockito.`when`(pm.getInstalledPackages(0)).thenReturn(installedApps) // 4

    assertTrue(MainActivity.isSampleAppInstalled(pm)) // 5
}
  1. PackageInfo 객체를 만듭니다.
  2. packageName에 "com.codechacha.sample"를 설정합니다.
  3. PackageInfo를 리스트로 만듭니다.
  4. getInstalledPackages(0)가 호출될 때 위에서 만든 리스트를 리턴하도록 합니다.
  5. 테스트할 API를 호출해서 true가 리턴되는지 확인합니다.

만약 테스트 실행 중에 아래와 같은 에러가 발생하면 위의 gradle 설정에서 소개한 returnDefaultValues = true를 설정해보고 다시 실행해보세요.

java.lang.RuntimeException: Method toString in android.content.pm.PackageInfo not mocked. See http://g.co/androidstudio/not-mocked for details.

at android.content.pm.PackageInfo.toString(PackageInfo.java)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)

정리

Mockito를 사용하여 Unit test를 작성하는 방법에 대해서 알아보았습니다. Mockito는 JVM에서 동작하는 Unit test와 실제 디바이스 또는 에뮬레이터에서 동작하는 Instrumentation test에 모두 사용할 수 있습니다. 테스트하려는 코드 외의 의존성들을 Mockito로 없애고, Unit을 테스트할 수 있습니다.

이 글에서 사용한 코드는 GitHub에서 확인할 수 있습니다.

참고