이 글의 모든 코드는 아래 저장소에서 확인할 수 있다.
https://github.com/linuxias/Android-Testing/tree/main/Testing_RoomDB
데이터베이스 코드
TestDao.kt
package com.linuxias.testing_roomdb.data.local
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface TestDao {
@Query("SELECT * FROM TestEntity")
fun getAll() : LiveData<List<TestEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertEntity(testEntity: TestEntity)
@Delete
suspend fun deleteEntity(testEntity: TestEntity)
}
TestDatabase.kt
package com.linuxias.testing_roomdb.data.local
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(
entities = [TestEntity::class],
version = 1
)
abstract class TestDatabase : RoomDatabase() {
abstract fun testDao(): TestDao
}
TestEntity.kt
package com.linuxias.testing_roomdb.data.local
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class TestEntity (
@PrimaryKey(autoGenerate = true) var id : Int? = null,
@ColumnInfo(name="title") val title : String,
@ColumnInfo(name="content") val content : String,
@ColumnInfo(name="importance") val importance : Int,
@ColumnInfo(name="due") val due : String
)
테스트 코드 분석
전체 테스트 코드 전에 테스트 셋업 단계 코드부터 살펴본다. 각 테스트를 독립적으로 사용하기 위한 방식과 @Before, @After 애노테이션을 통한 객체 관리를 정리한다.
@RunWith(AndroidJUnit4::class)
@SmallTest
class TestDaoTest {
private lateinit var database: TestDatabase
private lateinit var dao: TestDao
@Before
fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
TestDatabase::class.java
).allowMainThreadQueries().build()
dao = database.testDao()
}
@After
fun teardown() {
database.close()
}
}
@RunWith(AndroidJUnit4.class)
JUnit 라이브러리는 기본적으로 자바와 코틀린 코드를 jvm에서 테스트하기 위한 환경을 제공하는 라이브러리이다. 하지만 Room Database와 같은 컴포넌트를 테스트하기 위해서는 안드로이드의 컨텍스트 등이 필요로 하고, jvm 환경에서는 테스트를 할 수 없다. 우리는 jvm 환경에서 테스트 하려는 것이 아니라 안드로이드 내부 환경에서 테스트 하려한다. 해당 환경에서 테스트하기 위해 우리는 Instrumented unity test를 작성한다. 그리고 테스트 클래스 상단에 @RunWith(AndroidJUnit4.class) 애노테이션을 추가하여 안드로이드 타겟 또는 에뮬레이터에서 테스트하는 클래스라는 것을 명시해줌으로써 안드로이드 타겟 환경에서 테스트할 수 있도록 한다.
@SmallTest, MediumTest, LargeTest
테스트 클래스 상단에 @SmallTest 애노테이션을 추가하였다. @SmallTest (또는 @MediumTest, @LargeTest) 애노테이션은 필수가 아닌 옵션이지만, 추가하는 것을 권장한다. 앞서 설명한 Test의 종류에 대해 기억하는가? (참고 : https://sonseungha.tistory.com/630)
테스트 피라미드에서 Unit tests, Integration test, UI test로 분리하여 설명하였었다. 각 테스트 피라미트는 테스트의 크기로 분류가 가능하다.
- Small Test : 한 번에 한 클래스씩 앱 동작의 유효성을 검사하는 단위 테스트입니다.
- Medium Test : 모듈 내의 스택 수준 간 상호작용 또는 관련 모듈 간 상호작용의 유효성을 검사하는 통합 테스트
- Large Test : 앱의 여러 모듈에 걸쳐 사용자 시나리오의 유효성을 검사하는 End-To-End 테스트
좀 더 상세한 분류는 Android Test Blog 에 잘 작성되어있다.
여기서는 Room Database의 기본적인 단위테스트를 수행할 것이기에 @SmallTest 애노테이션만을 추가하였다.
allowMainThreadQueries()
Database 객체를 생성할 때 allowMainThreadQueries() 함수를 사용하여 메인스레드에서 동작하도록 설정한다. 보통 데이터베이스 (Room database 포함하여)를 접근(access)하는 경우에는 백그라운드 스레드를 사용한다. 이유는 모두 알고있듯이 데이터베이스에 접근, 쓰기, 읽기 등의 동작은 스레드 블록(Blocking)을 발생시키기에 메인스레드에서 동작 시 해당 문제는 사용자 경험을 떨어뜨리기 때문에 백그라운드 서비스에서 동작시킨다.
하지만 이 테스트에서는 명확하게 싱글스레드에서 동작시키길 원한다. 멀티스레드 환경에서 여러 스레드로 동작 시 각 스레드 간 테스트에 어떤 영향을 줄 지 우리는 파악하기 어렵다. 이러한 이유로 테스트가 모두 독립적으로 동작한다고 판단내리기 어렵기에 메인스레드에서 테스트를 동작시켜 테스트가 하나씩 독립적으로 수행됨을 보장하고자 한다.
전체 테스트 코드 분석
전체 테스트 코드를 분석하기 전에, LiveData 테스트를 위해 하나의 파일을 추가한다. LiveData에 관한 결과를 얻기 위해서 LiveData에 대한 결과를 관찰하고 있어야 한다. getOrAwaitValue함수 매핑하여 수정한다. 를 이용하여 대신합니다.
package com.linuxias.testing_roomdb
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
@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
}
이제 테스트 전체 코드를 확인한다. 테스트는 간단하다. TestEntity를 생성하고 데이터베이스에 삽입, 삭제 테스트를 진행한다.
package com.linuxias.testing_roomdb.data.local
import ...
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class TestDaoTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var database: TestDatabase
private lateinit var dao: TestDao
@Before
fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
TestDatabase::class.java
).allowMainThreadQueries().build()
dao = database.testDao()
}
@After
fun teardown() {
database.close()
}
@Test
fun insertTestEntity() = runTest {
val testEntity = TestEntity(1, "Title", "Content", 1, "2022-09-30")
dao.insertEntity(testEntity)
val allTestEntities = dao.getAll().getOrAwaitValue()
assertThat(allTestEntities).contains(testEntity)
}
@Test
fun deleteTestEntity() = runTest {
val testEntity = TestEntity(1, "Title", "Content", 1, "2022-09-30")
dao.insertEntity(testEntity)
dao.deleteEntity(testEntity)
val allTestEntities = dao.getAll().getOrAwaitValue()
assertThat(allTestEntities).doesNotContain(testEntity)
}
@Test
fun getAllTest() = runTest {
val testEntity1 = TestEntity(1, "Title1", "Content1", 1, "2022-09-30")
val testEntity2 = TestEntity(2, "Title2", "Content2", 2, "2022-10-30")
val testEntity3 = TestEntity(3, "Title3", "Content3", 3, "2022-11-30")
dao.insertEntity(testEntity1)
dao.insertEntity(testEntity2)
dao.insertEntity(testEntity3)
val allTestEntities = dao.getAll().getOrAwaitValue()
assertThat(allTestEntities).contains(testEntity1)
assertThat(allTestEntities).contains(testEntity2)
assertThat(allTestEntities).contains(testEntity3)
}
}
결론
누군가는 room database는 구글 팀에서 별도로 개발 및 관리를 하는데 왜 테스트를 하는지 묻기도 한다. 이 예젠에서는 매우 단순한 쿼리에 대해서만 작성하고 테스트를 진행하기에 문제가 없어보이지만 실제로 프로젝트를 진행 시 매우 복잡한 쿼리를 작성하게 되고 다양한 테이블 간의 조인 등을 통해 데이터를 확인해야 하는 경우도 있다.
이러한 경우 쿼리의 변경이나 데이터 테이블, 컬럼등의 수정 등이 기존의 데이터를 가져오던 쿼리에 영향을 주는지 확인하기 위해 테스트는 필수적이다.
본인의 프로젝트 중 아직 Room database에 테스트가 없다면 지금 작성해보면 좋지 않을까?
Reference
'Android > Testing' 카테고리의 다른 글
[Android/Testing] #6. 테스트 시작 전에 테스트 더블(Test Double) 이해하기 (0) | 2022.10.08 |
---|---|
[Android/Testing] #5. 테스트를 위한 기반 프로젝트 생성하기 (0) | 2022.10.02 |
[Android/Testing] #3. Unit Testing 입문하기 (0) | 2022.09.15 |
[Android/Testing] #2. 좋은 테스트는 어떻게 작성해야 할까? (1) | 2022.09.12 |
[Android/Testing] #1. 왜 테스트를 해야 하는가? (0) | 2022.09.06 |