이 글은 Udacity Android Kotlin Developer 강좌를 수강하고 난 뒤 개인적인 학습을 위해 정리한 내용입니다. 틀린 내용이 있을 수 있습니다.
안드로이드 테스트 코드 읽기 쉽게 작성하기
안드로이드 개발에서 테스트 코드는 프로덕트 코드의 문서 역할을 할 수가 있다. 프로덕트 코드가 어떤 역할을 하는지 테스트 코드를 통해 보여줌으로써 프로덕트 코드를 설명할 수 있다. 테스트 코드는 프로덕트 코드가 변함에 따라 테스트 성공을 위해 반드시 갱신되어야 한다는 점에서 항상 최신화가 되는 문서가 된다는 장점이 있다.
그러므로 문서의 기능도 수행할 수 있는 테스트 코드를 다른 사람이 읽기 쉽게 작성하는 것이 좋다는 것은 당연하다. 읽기 쉬운 테스트 코드 작성을 위해서 다음과 같은 3가지 방법을 사용할 수 있다.
테스트 함수 네이밍 (subjectUnderTest_actionOrInput_resultState)
테스트 함수를 다음과 같은 네이밍 구조로 이름 지으면 좋다.
subjectUnderTest_actionOrInput_resultState
- subjectUndetTest : 테스트를 수행할 대상을 의미한다.
- actionOrInput : 테스트할 동작이나 대상에 전달하는 input 데이터를 의미한다.
- resultState : 동작이 수행된 이 후 예상하는 결과를 의미한다.
테스트 함수 구조화 (Given, When, Then or Arange, Act, Assert)
테스트 함수의 내부 구조를 다음과 같은 형태로 구조화하고, 주석을 달면 읽기 쉬워진다.
- Given : 테스트를 진행할 전제 조건을 의미한다.
- When : 테스트의 대상이 되는 동작이 수행되었을때를 의미한다.
- Then : 테스트 대상 동작이 수행된 이후 결과를 예측하는 것을 의미한다. assert 동작들.
유용한 Test Framework 사용 (Hamcrest, Truth)
테스트 코드를 조금 더 인간의 언어와 비슷한 형태로 작성할 수 있도록 도와주는 프레임워크들이 있다. Hamcrest 도 그 중에 하나이며 Hamcrest 사용시 다음과 같은 의존성을 추가해야한다.
testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
Hamcrest 를 사용하면 테스트 코드를 좀 더 인간 영어 문법에 가깝게 작성할 수 있다.
assertEquals(1 + 1, 2)
assertThat(1 + 1, `is`(s))
Test Scope
Test Scope 란 테스트 코드가 프로덕트 코드의 어느 부분까지 커버할 것인지를 의미한다. 다음과 같은 종류가 있다.
- Unit Test : 하나의 함수나 클래스를 테스트하는 것
- 로컬에서 실행되기 때문에 빠르다.
- fidelity 가 낮다.
- Integration Test : 하나의 동작을 수행하기 위해 필요한 함수나 클래스들의 integration 을 테스트하는 것이다.
- 로컬에서 실행될 수도 있고, 단말기기에서 실행될 수도 있다.
- fidelity 가 비교적 높다.
- End-to-End Test : 앱의 전체적인 동작을 테스트하는 것이다.
- 단말 기기에서 실행되기 때문에 느리다.
- fidelity 가 높다.
테스트 코드를 작성함에 있어서 다음과 같은 비율로 테스트 코드를 구성하는 것이 바람직하다.
- Unit Test : 70%
- Integration Test : 20%
- End-to-End Test : 10%
이러한 테스트 코드 구성은 프로젝트의 아키텍쳐링으로 부터 비롯된다. 만약 좋은 아키텍처링을 채택하고 있지 않은 상황에서 테스트 코드를 작성하기 위해서는 어떻게 해야할까? 이때는 리펙토링부터 진행하기 보다 다음과 같이 테스트 코드를 작성하면서 진행하는 것이 버그를 줄이는데 도움이 된다.
- 쉽게 테스트 할 수 있는 Utility 클래스나 함수의 테스트를 먼저 작성한다.
- End-to-End 테스트를 작성한다.
- 리펙토링을 수행한다.
- Unit, Integration 테스트를 작성한다.
ViewModel Local 테스트하기
ViewModel 에 있는 단일 메소드를 테스트하는 Unit 테스트는 다음과 같이 테스트할 수 있다. 일단 ViewModel 자체는 AndroidFramework 코드에 의존하지 않고 작성해야한다. 그렇기 때문에 ViewModel 의 Unit Test 를 Test source set 에 포함시켜서 작성할 수 있다.
그러나 ViewModel 이 AndroidViewModel 을 상속하는 경우라면 ViewModel 생성자에 applicationContext 를 넘겨줘야 한다. Unit Test 에서 Android Framework 코드가 필요한 상황이라면 다음과 같은 과정을 거쳐서 해결할 수 있다.
- Android X Test Library 를 test source set 에 포함 시키기
- Robolectrict Test Library 를 test source set 에 포함 시키기
- Android X Test Library 를 이용해서 application context 이용하기
- AndroidJUnit test runner 설정
Android X, Robolectrict Test Library 를 test source set 에 포함 시키기
dependencies {
// AndroidX Test - JVM testing
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
}
Android X Test Library 를 이용해서 application context 이용하기, AndroidJUnit test runner 설정
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh TasksViewModel
val viewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
// Then the new task event is triggered
}
}
추가 설명
Robolectric Library
Robolectric
Running tests on an Android emulator or device is slow! Building, deploying, and launching the app often takes a minute or more. That’s no way to do TDD. There must be a better way. Robolectric is a framework that brings fast and reliable unit tests to A
robolectric.org
Robolectrict Library 는 JVM Test 환경에서 Android Framework 코드를 실행시킬 수 있도록 해주는 테스트 라이브러리이다. Android application class 나 Activity 클래스를 Jvm 환경에서 실행되는 테스트 코드에서 얻어올 수 있도록 도아준다.
Android X Library
테스트가 JVM 환경에서 실행되느냐, 안드로이드 단말 또는 에뮬레이터에서 실행되느냐에 상관없이 동일한 Test api 를 사용하여 테스트 코드를 작성할 수 있도록 도와주는 라이브러리이다.
@RunWith(AndroidJUnit4::class)
- @RunWith 는 안드로이드 테스트 코드를 실행하는 기본 컴포넌트를 다른 컴포넌트로 대체하는 것을 의미하는 어노테이션이다.
- AndroidJUnit4 Test Runner 는 이 테스트가 로컬 JVM 환경에서 실행되는지, 에뮬레이터에서 실행되는지에 따라서 Android X api 를 Instrumented Test api 로 변환할지, Robolectric api 로 변환할 지를 결정한다.
- 이 같은 설정을 통해서 테스트를 로컬 JVM 환경과 Instrumented 테스트 환경 사이에서 쉽게 변경할 수 있고, 개발자는 하나의 테스트 api 만을 배워서 로컬, instrumented 테스트 모두를 작성할 수 있게된다.
LiveData 테스트하기
Android LiveData 를 테스트하기 위해서는 다음 2가지를 꼭 확인해야한다.
- InstantTaskExecutorRule 추가하기
- LiveData Observe 하기
InstantTaskExecutorRule 추가하기
- JUnit Test Rule 은 각각의 테스트가 실행되기전, 실행된 후에 실행되어야 하는 로직을 정의할 수 있도록 하는 것이다.
- Android Architecture Component 는 백그라운드 작업 처리 동시에 수행할 수 있는 Background Executor를 가지고 있다.
- InstantTaskExecutorRule 은 테스트 코드가 항상 동일한 순서대로 실행되고, synchronous 한 순서대로 실행될 수 있도록 Android Architecture Component 에서 사용하는 Task Executor를 다른 executor 로 변경할 수 있게 해준다.
InstantTaskExecutorRule 를 사용하기 위해서는 다음 dependency 를 추가해야 한다.
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
실제 테스트코드에서 test Rule 추가는 다음과 같은 형태로 추가한다.
class TasksViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Other code…
}
LiveData Observe 하기
LiveData 는 Observe 하지 않으면 값을 emit 하는 동작이 수행되지 않을 수 있다. test code 에서 fragment 나 activity 없이 live data 를 observe 하려면 많은 boiler plate 코드가 필요한데, 다음 확장함 수를 추가하여 해결할 수 있다.
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}