#1 이전 글
#1-1 Unit Testing 개요
위 링크에 있는 이전 게시글에 이어서, 실제 안드로이드 프로젝트를 만들어 Room 및 LiveData의 Unit Testing을 수행해본다. 본 게시글을 읽기 전에 [Android] Unit Testing - 기초를 보고 오면 이해에 도움이 된다.
#1-2 환경 설정 (build.gradle 등)
이전 게시글의 #3을 토대로 본 게시글에 나오는 안드로이드 프로젝트의 Gradle, AGP, JDK의 버전 설정 및 build.gradle 설정을 진행한다. 이전 게시글의 build.gradle과 달리, 본 게시글에서는 Unit Testing에 사용되지 않는 build.gradle의 plugins { ... }, buildFeatures { ... }, dependencies { ... }의 일부 요소를 제거했다. 이는 코드 다이어트를 위한 개인적인 제거이기 때문에, 이 글은 보는 사람은 제거 없이 그냥 복사 및 붙여넣기해도 된다.
#2 Unit Testing을 적용할 샘플 앱
이 게시글의 완성된 앱을 가져와서 테스트를 수행한다. 해당 앱을 가져와서 #1-2에 맞게 환경 설정을 수정했다. 전체 소스 코드는 본 게시글의 #5에 있다.
#3 Unit Testing
#3-1 테스트 클래스 생성
내가 테스트할 클래스는 UserDAO다. 이는 인터페이스이기에 이 게시글의 #3-1에서 진행한 것처럼 테스트 클래스 생성을 할 수는 없다 ([Generate...] 메뉴 다음으로 [Test...] 메뉴가 안 뜸). 하지만, 그냥 직접 수동으로 테스트 클래스를 만들면 그만이다. Room 라이브러리를 사용해야하기 때문에 Instrumented test로 진행한다. 따라서, androidTest 디렉토리에 테스트 클래스를 생성한다. LiveDataTestUitl.kt라는 파일도 보이는데, 곧바로 설명하자면
#3-2 LiveDataTestUitl
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
// 코드 출처: https://stackoverflow.com/questions/73292145/android-unit-testing-kotlin-livedata-value-was-never-set
@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
}
여기에 있는 소스 코드를 가져왔다. 이 클래스는 LiveData를 테스트할 때 유용하다. 본 게시글에서 사용한 샘플 앱의 Room은 반환값으로 LiveData를 내뱉는다. 이 때, LiveData의 value를 참조하려면, observe()를 통해, LiveData의 값이 변화하는 순간을 포착해야한다. 하지만, 테스트 클래스에서 observe() 등을 구현하는 것은 귀찮은 일이다. LiveData.observe()의 동작엔 Lifecycle이 관여하기 때문이다. LiveDataTestUitl 클래스의 getOrAwaitValue()는 LiveData의 value를 직관적으로 가져오게 만들어준다.
#3-3 테스트 클래스의 내용 작성
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.testingroom.getOrAwaitValue
import com.google.common.truth.Truth
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/* @RunWith 어노테이션이 없으면,
* JUnit이 기본 테스트 러너(Test Runner)인 JUnit4 테스트 러너를 사용한다.
* AndroidJUnit4 테스트 러너와는 달리, JUnit4 테스트 러너는 안드로이드 환경에 맞춰져 있지 않으므로,
* 에러의 여지가 생겨버린다.
*/
@RunWith(AndroidJUnit4::class)
class UserDAOTest {
/* InstantTaskExecutorRule 규칙(Rule)
* 기본적으로 백그라운드 스레드에서 실행되는 LiveData를 메인 스레드에서 실행되도록 강제함.
* LiveData가 백그라운드 스레드에서 그 값이 변경된다면, 메인 스레드에서 변화를 감지하지 못할 수도 있기 때문.
* 또, 데이터의 변화를 감지하기 전에 테스트가 끝나버릴 여지도 있음.
* 따라서, LiveData가 관여되는 테스트에는 InstantTaskExecutorRule 규칙이 많이 사용됨.
*/
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var dao: UserDAO
private lateinit var database: UserDatabase
@Before
fun setUp() {
// Dependency 초기화
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
UserDatabase::class.java
).build()
// Dependent 초기화 (인터페이스 구현체를 인터페이스 프로퍼티에 넣는 것도 '의존성 주입'의 한 갈래다)
dao = database.userDAO
}
@After
fun tearDown() {
database.close()
}
@Test
fun insertUsersTest() {
runBlocking {
// Insert
val usersToInsert = listOf(
User(1, "steve", "helloWorld@example.com"),
User(2, "bob", "pizza1234@example.com"),
User(3, "paul", "kenel@example.com"),
User(4, "kevin", "chickenMania@example.com")
)
dao.insertUser2(usersToInsert)
/* Get
* 원래 LiveData의 value를 참조하려면, observe()를 통해,
* LiveData의 값이 변화하는 순간을 포착해야한다.
* 그 과정을 LiveDataTestUtil에서 알아서 진행해준다.
* 만약 LiveDataTestUtil의 함수 없이,
* val usersFetched = dao.getAllUsers().value와 같은 식의 코드였다면,
* usersFetched에는 null이 담기게 된다.
*/
val usersFetched = dao.getAllUsers().getOrAwaitValue()
// 비교
Truth.assertThat(usersFetched).isEqualTo(/* expected = */ usersToInsert)
}
}
@Test
fun deleteUsersTest() {
runBlocking {
// Insert
val usersToInsert = listOf(
User(1, "steve", "helloWorld@example.com"),
User(2, "bob", "pizza1234@example.com"),
User(3, "paul", "kenel@example.com"),
User(4, "kevin", "chickenMania@example.com")
)
dao.insertUser2(usersToInsert)
// Delete
dao.deleteAll()
// Get을 통한 확인
val usersFetched = dao.getAllUsers().getOrAwaitValue()
Truth.assertThat(usersFetched).isEmpty()
}
}
}
Room.inMemoryDatabaseBuilder()는 1회용 데이터베이스 영역을 생성한다. 테스트 종료 시에 해당 데이터베이스는 제거되므로 본래의 데이터베이스에 교란을 일으킬 여지가 없다.
여담으로 Room.inMemoryDatabaseBuilder를 사용하지 않고, UserRepository를 상속받는 StubUserRepository를 만드는 방식도 사용할 수 있다. 이 경우는 Room을 사용하지 않기 때문에 Instrumented test가 아니라 Local unit test로 진행이 가능하다.
#4 작동 확인
#5 완성된 앱
'깨알 개념 > Android' 카테고리의 다른 글
[Android] Data Binding - View Binding (0) | 2024.07.10 |
---|---|
[Android] Unit Testing - Retrofit (0) | 2024.07.08 |
[Android] Unit Testing - ViewModel (0) | 2024.07.08 |
[Android] Unit Testing - 기초 (0) | 2024.07.08 |
[Android] Unit Testing - 개요와 환경 설정 (0) | 2024.07.03 |