Unit Test와 Android Framework
Android 개발자들에게 Test가 어렵게 다가오는 이유 중 하나는 안드로이드에서는 조금만 잘못해도 테스트가 실패하기 때문이다. 이번 글에서는 대부분의 안드로이드 개발자들이 겪는 안드로이드에서 테스트가 실패하는 이유 두가지와 해결 방법에 대해 알아볼 것이다.
안드로이드에서 테스트가 실패하는 케이스
안드로이드에서 안드로이드로 인해 테스트가 실패하는 케이스는 두가지이다. 바로 안드로이드에서 제공하는 정적 메서드를 사용하는 경우와, 안드로이드 전용 클래스를 사용하는 경우이다. 이들 각각에 대한 테스트를 작성해보고 왜 실패하는지 이유와 해결책을 알아보자.
안드로이드에서 제공하는 정적 메서드 테스트 해보기
예를 들어 유저가 가입할 때 id가 유효한지를 확인하는 IdChecker 클래스를 만들었다고 해보자. isValid 는 id가 공백이 아닐 때 true를 반환하며, android.text 패키지의 TextUtils의 정적 메서드를 사용한다.
import android.text.TextUtils
class IdChecker {
fun isValid(id: String) : Boolean {
return !TextUtils.isEmpty(id)
}
}
위 메서드를 테스트 하기 위해서는 id가 빈 값이 아닐 때 true를 반환하는 것을 Assert 하면 된다. 따라서 다음과 같이 테스트를 작성하면 통과해야 한다.
class AndroidSpecificTest {
@Test
fun testValidIdReturnsTrue(){
val idChecker = IdChecker()
val isValid = idChecker.isValid("test")
assertTrue(isValid)
}
}
하지만 실행해보면 결과는 다음과 같이 Fail 되는 것을 볼 수 있다.
안드로이드 정적 메서드 사용 시 테스트가 실패하는 이유
위의 테스트 실패에 대한 자세한 이유를 알기 위해서는 아래 링크를 참조하자.
위 링크에서는 RuntimeException의 오류의 이유가 다음과 같다고 한다.
The android.jar file that is used to run unit tests does not contain any actual code - that is provided by the Android system image on real devices. Instead, all methods throw exceptions (by default).
유닛테스트 실행을 위한 안드로이드의 jar 파일은 어떠한 실제 코드도 포함하지 않는다 - 이 코드들은 실제 디바이스의 안드로이드 시스템 이미지에 의해 제공된다. 대신, 모든 메서드는 기본적으로 예외를 던진다.
즉, 안드로이드와 관련된 클래스와 메서드들은 안드로이드 기기마다 다르게 구현되어 있고, 안드로이드 기기에 의해 제공되기 때문에 유닛 테스트 시에는 실제 디바이스가 없기 때문에 예외를 던져서 테스트가 불가능 하다는 것이다.
만약 기본 라이브러리를 제공해 테스트를 가능하게 한다면 기기마다 다르게 동작하게 될 것이기 때문에 테스트 자체가 의미가 없어진다.
정적 메서드 오류 해결 방법
정적 메서드 사용시 테스트에 오류가 생기는 것을 해결하기 위해서는 정적 메서드를 없애는 수 밖에 없다. 메서드는 Mocking 할 수 없기 때문에 응답을 임의로 설정하는 것이 불가능 하기 때문이다.
보통 정적 메서드들은 기본 패키지에 비슷한 기능을 하는 메서드가 있는 경우가 많다. 따라서 이로 교체하자. 예를들어 !TextUtils.isEmpty(id)는 Kotlin Standard Library의 isNotEmpty로 교체될 수 있다.
class IdChecker {
fun isValid(id: String) : Boolean {
return id.isNotEmpty()
}
}
수정 후 테스트하면, 테스트가 성공하는 것을 볼 수 있다.
안드로이드 전용 클래스 사용시 문제 현상
안드로이드 전용 클래스를 코드 내에서 사용해도 안드로이드 정적 메서드를 사용할 때와 같은 문제가 생긴다. 대표적인 것이 Intent와 Bundle이다. 예를 들어 Bundle()을 사용하는 BundleUseCase 클래스를 테스트를 한다고 해보자.
class BundleUseCase() {
private val bundle = Bundle()
fun getStringFromBundle(id: String): String {
return bundle.getString(id, "")
}
}
위 코드에 대한 테스트에서 BundleUseCase를 초기화 후 메서드를 수행하면 오류가 생긴다. Assert를 하지 않더라도 오류가 생긴다.
@Test
fun getStringFromBundle() {
val bundleUseCase = BundleUseCase()
bundleUseCase.getStringFromBundle("id")
}
오류 메세지는 위 정적 메서드와 같다. 이유 또한 같다.
안드로이드 전용 클래스 사용은 해결할 수 있다
하지만 정적 메서드는 해결 방법이 없던 반면, 전용 클래스는 해결 방법이 있다. 바로 Android 전용 클래스를 생성자 부분으로 이동시키는 것이다. 즉, BundleUseCase을 다음과 같이 바꾼다.
class BundleUseCase(
private val bundle: Bundle
) {
fun getStringFromBundle(id: String, string: String): String {
return bundle.getString(id, "")
}
}
그러면 주입을 받은 객체에 대해서는 Mocking이 가능해진다. 즉, Mock된 안드로이드 전용 객체(Bundle)에 특정 요청에 대한 응답을 정의하는 것이 가능해진다.
@RunWith(MockitoJUnitRunner::class)
class AndroidSpecificTest {
@Mock
lateinit var bundle: Bundle
@Test
fun testAndroidBundler() {
val bundler = BundleUseCase(bundle)
`when`(bundle.getString("id", "")).thenReturn("test")
}
}
안드로이드 전용 클래스에 대해 안드로이드 기기의 구현체가 없더라도 Stub을 정의함으로써 문제를 없앨 수 있는 것이다. 따라서 아래와 같이 테스트를 작성하면 테스트가 통과된다.
@RunWith(MockitoJUnitRunner::class)
class AndroidSpecificTest {
@Mock
lateinit var bundle: Bundle
@Test
fun testAndroidBundler() {
val bundler = BundleUseCase(bundle)
`when`(bundle.getString("id", "")).thenReturn("test")
val result = bundler.getStringFromBundle("id", "")
assertEquals("test", result)
}
}
정리
안드로이드 개발 환경에서 테스트를 작성할 때 마주할 수 있는 문제를 알아보았다. 바로 안드로이드의 패키지 내부의 정적 메서드를 사용할 때의 문제와 안드로이드 패키지 내부에 정의된 클래스를 사용할 때의 문제이다. 이 두 문제는 모두 안드로이드 기기에서 구현체를 제공하기 때문에 테스트 환경에서는 구현체가 없어서 생기는 문제이다.
정적 메서드의 경우 Mocking이 불가능해 임의의 응답을 정의할 수 없어 문제를 해결하기 위해서는 정적 메서드를 제거해야 하지만, 안드로이드 전용 클래스의 경우에는 Mocking이 가능해 생성자 주입을 통해 안드로이드 전용 클래스를 사용한다면 문제를 해결하는 것이 가능하다.
'Unit Testing' 카테고리의 다른 글
[Unit Testing] 어떤 클래스에 테스트가 필요할까? data class 도 테스트가 필요한가? (0) | 2022.12.28 |
---|---|
[JUnit] Unit Testing을 깔끔하게 작성하는 방법 : 테스트 이름 작성 방법, 코드 순서 작성 방법 (0) | 2022.12.27 |
[Unit Testing] Android Context 객체를 사용하는 클래스 테스트하기 (0) | 2022.12.25 |
코드 작성 시 정적 변수와 정적 메서드 사용을 지양해야 하는 이유 알아보기 (0) | 2022.12.24 |
MockitoJUnitRunner 사용해 Mockito 코드 깔끔하게 만들기 : @Mock (0) | 2022.12.23 |