Android/테스팅

안드로이드 테스트 코드를 배워보자 (2) Room Unit Test

sh1mj1 2023. 10. 10. 17:21

https://sh1mj1-log.tistory.com/175

 

안드로이드 테스트 코드를 배워보자 (1) - liveData 테스트, 비동기 테스트 기본

안드로이드 앱을 개발할 때 여러 기업에서, 프로젝트에서 테스트 코드를 작성하는 것은 중요하다고 말합니다. 아예 앱을 개발할 때 Test code 를 먼저 작성하는 경우도 있죠. 실제로 카카오에서 티

sh1mj1-log.tistory.com

이전 글에서 이어집니다.

 

커여운 고양이 짤

 

Room Unit Test -  ViewModel, LiveData 등 사용

Room DB 는 안드로이드 Jetpack Components 입니다. 그러므로 안드로이드 종속성이 필요합니다.

안드로이드 개발자 문서 -  Room 지속성 라이브러리는 SQLite에 추상화 계층을 제공하여 SQLite를 완벽히 활용하면서 더 견고한 데이터베이스 액세스를 가능하게 합니다.

 

gradle 에 Room 종속성 추가

dependencies {
    def room_version = "2.5.0"

    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
    ...

    // To use Kotlin annotation processing tool (kapt)
    kapt "androidx.room:room-compiler:$room_version"
    
    // 코루틴 기능을 사용하기 위해서는 ktx artifact 를 Room 추가
    implementation("androidx.room:room-ktx:$room_version")

}

 

 

만약 kapt 를 추가하지 않았다면 `build.gradle` 상단에 아래 `plugins` 도 추가해 주어야 합니다.

plugins {
    ...
    id 'kotlin-kapt'
}

추가해주어야 합니다. 

 

즉, Room android Test 를 하려면 AndroidTest 디렉토리에 테스트 코드를 만들어야 하죠. 코드는 이 안드로이드 개발자 문서의 코드를 참조하여 변형했습니다. (안드로이드 개발자 문서 코드 깃허브)

 

그 전에 먼저 테스트할 데이터 클래스, 데이터베이스 클래스, Room DAO 클래스를 만들어야 합니다.

 

`User` 데이터 클래스

@Entity(tableName = "users")
data class User (
    @PrimaryKey
    @ColumnInfo(name = "userid")
    val id: String = UUID.randomUUID().toString(),
    @ColumnInfo(name = "username")
    val userName: String
)

 

`UserDatabase` 데이터베이스 클래스

@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        @Volatile
        private var INSTANCE: UserDatabase? = null

        fun getInstance(context: Context): UserDatabase =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
            }

        private fun buildDatabase(context: Context): UserDatabase =
            Room.databaseBuilder(
                context.applicationContext,
                UserDatabase::class.java, "Sample.db"
            )
                .build()
    }
}

 

`UserDao` dao 클래스

@Dao
interface UserDao {
    @Query("SELECT * FROM Users")
    suspend fun getAllUsers(): List<User>

    @Query("SELECT * FROM Users WHERE userid = :id")
    suspend fun getUserById(id: String): User

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(user: List<User>)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User)

    @Query("DELETE FROM Users")
    suspend fun deleteAllUsers()
}

 

이렇게 간단한 `User ` 관련 dao, database 까지 만들었습니다.

 

Test 작성하기

이제 Test 클래스를 만들어 봅시다.  먼저 코드부터 작성하고 봅시다.

 

`MovieDaoTest`

@RunWith(AndroidJUnit4::class)
class RoomDaoTest {
    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()

    private lateinit var dao: UserDao
    private lateinit var database: UserDatabase

    @Before
    fun setUp() {
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            UserDatabase::class.java
        ).build()
        dao = database.userDao()
    }

    @After
    fun tearDown() {
        database.close()
    }

    @Test
    fun saveUsersTest() = runBlocking {
        val users = listOf(
            User("1","username1"),
            User("2","username2"),
            User("3","username3")
        )
        dao.insertUsers(users)

        val allUsers = dao.getAllUsers()
        assertThat(allUsers).isEqualTo(users)
    }

    @Test
    fun deleteUsersTest() = runBlocking {
        val users = listOf(
            User("1","username1"),
            User("2","username2"),
            User("3","username3")
        )
        dao.insertUsers(users)
        dao.deleteAllUsers()
        val usersResult = dao.getAllUsers()
        assertThat(usersResult).isEmpty()
    }

}

 

Room 테스트 코드 프로세스는 아래와 같습니다.

  • `@Before` 에서 Room 데이터베이스와 DAO 를 미리 만들어 둔다.
  • Room DAO 메소드들을 테스트한 후에 `@After` 에서 데이터베이스 커넥션을 `close()` 해준다.

 

그런데 위 코드에서 낯선 코드들이 많습니다. 하나씩 차례대로 보시죠.

 

 

@RunWith(AndroidJUnit4::class) ??

공식 문서에서는 이렇게 설명하고 있습니다. (공식 문서)

`AndroidJUnitRunner` 클래스는 Espresso 및 UI Automator 테스트 프레임워크를 사용하는 것을 비롯하여 Android 기기에서 JUnit 3 또는 JUnit 4 스타일 테스트 클래스를 실행할 수 있는 JUnit 테스트 실행기입니다.
이 테스트 실행기는 테스트 패키지와 테스트 대상 앱을 기기에 로드하여 테스트를 실행하고 테스트 결과를 보고하는 역할을 합니다. 이 클래스는 JUnit 3 테스트만 지원하는 `InstrumentationTestRunner` 클래스를 대체합니다.

 

runBlocking ??

`runBlocking` 은 코틀린의 코루틴을 사용하는 동안 suspend 함수를 호출하는 코드 블록을 지원하는 함수입니다.

`runBlocking` 은 주로 테스트나 메인 함수처럼 비동기 코드를 호출하기 어려운 상황에서 간단하게 사용됩니다.

여기서 Room 에 접근하는 비동기 작업(`saveUsersTest` 함수 내에서 `dao.insertUsers(users)` 을 수행하고 있죠!

즉, 테스트 코드에서 코루틴을 사용하는 경우에 유용하며, 특히 비동기 작업을 동기적으로 따로 처리해야 하는 테스트에서 자주 사용됩니다. 

 

`runBlocking` 함수는 이전에 코루틴에 대해 알아볼 때 잠시 사용하고 간단히 설명한 적이 있습니다. ([코틀린] 그래서 코루틴이 뭔데?)

 

 

테스트를 실제로 돌려보면 아래처럼 결과가 나옵니다.

 

이전에 (test) 패키지에서 Unit Test 를 했을 때의 결과와 조금 다르게 나오는 것을 볼 수 있습니다. 그리고 앞에서 말했듯이 Room 관련 테스트를 진행할 때는 안드로이드 종속성이 필요하기 때문에 예뮬레이터가 실행되는 것도 볼 수 있습니다.

 

 

 

Reference Link

https://youngest-programming.tistory.com/492

https://developer.android.com/training/data-storage/room?hl=ko

https://github.com/android/architecture-components-samples/tree/main/BasicRxJavaSampleKotlin

https://developer.android.com/training/testing/junit-runner?hl=ko

https://sh1mj1-log.tistory.com/174