๊นจ์•Œ ๊ฐœ๋… ๐Ÿ“‘/Android

[Android] Unit Testing - Room๊ณผ LiveData

interfacer_han 2024. 7. 8. 12:37

#1 ์ด์ „ ๊ธ€

#1-1 Unit Testing ๊ฐœ์š”

 

[Android] Unit Testing - ๊ฐœ์š”์™€ ํ™˜๊ฒฝ ์„ค์ •

#1 ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ ํ…Œ์ŠคํŠธ#1-1 ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ ํ…Œ์ŠคํŠธ์˜ ์ข…๋ฅ˜๋จผ์ €, ์—ฌ๊ธฐ์— ์žˆ๋Š” ๊ตฌ๊ธ€ ๊ณต์‹ ๋ฌธ์„œ์—์„œ ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ๊ฐœ์š”๋ฅผ ์ฝ์œผ๋ฉด ์ข‹๋‹ค. ํ•ด๋‹น ๊ตฌ๊ธ€ ๊ณต์‹ ๋ฌธ์„œ์—์„œ ๋ณต์‚ฌํ•ด์˜จ ์œ„์˜ ๊ทธ๋ฆผ

kenel.tistory.com

์œ„ ๋งํฌ์— ์žˆ๋Š” ์ด์ „ ๊ฒŒ์‹œ๊ธ€์— ์ด์–ด์„œ, ์‹ค์ œ ์•ˆ๋“œ๋กœ์ด๋“œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด 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์„ ์ ์šฉํ•  ์ƒ˜ํ”Œ ์•ฑ

 

[Android] Room - ๊ธฐ์ดˆ, INSERT์™€ DELETE ์—ฐ์Šต

#1 Room ์†Œ๊ฐœ Room์„ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ์ดํ„ฐ ์ €์žฅ | Android Developers Room ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋” ์‰ฝ๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์œ ์ง€ํ•˜๋Š” ๋ฐฉ๋ฒ• ์•Œ์•„๋ณด๊ธฐ developer.android.com SQLite๋Š” ๋ชจ๋ฐ”์ผ ๊ธฐ๊ธฐ๋ฅผ ์œ„ํ•œ

kenel.tistory.com

์ด ๊ฒŒ์‹œ๊ธ€์˜ ์™„์„ฑ๋œ ์•ฑ์„ ๊ฐ€์ ธ์™€์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ํ•ด๋‹น ์•ฑ์„ ๊ฐ€์ ธ์™€์„œ #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-practice/unit-test/TestingRoomAndLiveData at master ยท Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com