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
}
}
- Mockito를 사용하려면 이렇게 Annotation을 붙여줍니다. (
@RunWith(AndroidJUnit4::class)
를 붙여도 동작하긴 하네요.) - Junit은
@Test
가 있는 메소드를 테스트 메소드라고 인식합니다. Example
클래스를 mocking하여 객체로 만들어줍니다.- 이 객체의
getId()
가 호출되었을 때 100을 리턴하도록 합니다. - 이 객체의
getUrl(100)
가 호출되었을 때https://codechacha.com
를 리턴하도록 합니다. 인자가 꼭 100으로 전달되어야 합니다. - getId()가 100을 리턴하는지 확인합니다.
- 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
}
getUrl(100)
이 호출되었는지 확인합니다.getId()
가 3번 호출되었는지 확인합니다. 2번만 호출되었다면 테스트는 fail됩니다.getId()
가 최소 2번은 호출되었는지 확인합니다.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
}
- PackageInfo 객체를 만듭니다.
- packageName에 "com.codechacha.sample"를 설정합니다.
- PackageInfo를 리스트로 만듭니다.
getInstalledPackages(0)
가 호출될 때 위에서 만든 리스트를 리턴하도록 합니다.- 테스트할 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에서 확인할 수 있습니다.
참고
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력