[Android] LiveData ํ…Œ์ŠคํŠธ

๐Ÿ’ก์šฐ์•„ํ•œํ…Œํฌ์ฝ”์Šค ๊ณผ์ •์—์‹œ ์ง„ํ–‰ํ•œ LiveData๋ฅผ ํฌํ•จํ•œ ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•˜์—ฌ ๊ธฐ๋กํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

LiveData Testing

  • ์•ˆ๋“œ๋กœ์ด๋“œ์˜ LiveData๋Š” Android์—์„œ UI์™€ ๋ฐ์ดํ„ฐ ์ƒํƒœ๋ฅผ ๊ด€์ฐฐํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.
  • LiveData๋Š” Lifecycle์— ์˜์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ง์ ‘ ํ…Œ์ŠคํŠธํ•˜๊ธฐ์—๋Š” ๋ณต์žกํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋ผ์ดํ”„์‚ฌ์ดํด์„ ์ˆ˜๋™์œผ๋กœ ์กฐ์ž‘ํ•˜๊ฑฐ๋‚˜ JUnit๊ณผ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธ ๋„๊ตฌ๋ฅผ ํ™œ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

JUnit4 +InstantTaskExecutorRule

  • LiveData๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  • InstantTaskExecutorRule๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ด๋ฅผ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์ œ์–ด ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋ฉฐ, ์Šค๋ ˆ๋“œ ๋ฌธ์ œ๋ฅผ ํšŒํ”ผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • InstantTaskExecutorRule์€ JVM ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ Android Main Thread๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•ด์ค๋‹ˆ๋‹ค.

Gradle

testImplementation("androidx.arch.core:core-testing:2.2.0")

JUnit4 - Rule

class MainViewModelTest {
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()
    
    @Test
    fun testLiveData() {
        // Given
        // LiveData ์ƒ์„ฑ
        val liveData = MutableLiveData<String>()
        val observer = Observer<String> {}
        
        // When
        liveData.observeForever(observer)
        liveData.value = "Hello, World!"

        // Then
        assertEquals("Hello, World!", liveData.value)

        liveData.removeObserver(observer)
    }
}

 

Junit5 + InstantTaskExecutorExtension

  • JUnit5์—์„œ๋Š” JUnit 4์˜ Rule ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ, ์ง์ ‘ ์ œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

InstantTaskExecutorExtension

  • Callback์„ ์ƒ์†๋ฐ›์•„ ๊ตฌํ˜„ํ•˜๋ฉฐ, ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ๋™์ž‘ํ•˜๋„๋ก ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
class InstantTaskExecutorExtension : BeforeEachCallback, AfterEachCallback {
    override fun beforeEach(context: ExtensionContext?) {
        ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
            override fun executeOnDiskIO(runnable: Runnable) {
                runnable.run()
            }

            override fun postToMainThread(runnable: Runnable) {
                runnable.run()
            }

            override fun isMainThread(): Boolean {
                return true
            }
        })
    }

    override fun afterEach(context: ExtensionContext?) {
        ArchTaskExecutor.getInstance().setDelegate(null)
    }
}

in Test

  • ExtendWith์„ ํ™œ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
@ExtendWith(InstantTaskExecutorExtension::class)
class MainViewModelTest

 

getOrAwaitValue

  • LiveData์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์–ป์–ด์˜ค๊ธฐ ์œ„ํ•ด ํ•ด๋‹น ๊ฐ์ฒด๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ฒดํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • getOrAwaitValue๋Š” LiveData ๊ฐ’์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ํ…Œ์ŠคํŠธํ•  ๋•Œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • getOrAwaitValue ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด LiveData ๊ฐ’์„ ๊ด€์ฐฐํ•˜๊ณ , ๊ทธ ๊ฐ’์ด ์„ค์ •๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(t: T?) {
            data = t
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }

    this.observeForever(observer)

    if (!latch.await(time, timeUnit)) {
        throw TimeoutException("LiveData value was never set.")
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

in Test

  • ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ฐ’์„ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.MutableLiveData
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test

class LiveDataTest {

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Test
    fun testLiveData_withGetOrAwaitValue() {
        val liveData = MutableLiveData<String>()

        liveData.value = "Hello, World!"
        val value = liveData.getOrAwaitValue()

        assertEquals("Hello, World!", value)
    }
}

 

Robolectric

  • Robolectric๋Š” JVM ๋‚ด์—์„œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋œ ์•ˆ๋“œ๋กœ์ด๋“œ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
    • ์•ˆ๋“œ๋กœ์ด๋“œ ์˜์กด ํ…Œ์ŠคํŠธ๋„ JVM ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Robolectric๋ฅผ ํ™œ์šฉํ•˜๋ฉด LiveData์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์‰ฝ๊ฒŒ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์‹ค์ œ LiveData๊ฐ€ ์ƒ๋ช…์ฃผ๊ธฐ์— ๋”ฐ๋ผ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

in Test

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleRegistry
import org.junit.Test
import org.junit.Assert.*
import org.junit.Before
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.junit.runner.RunWith

@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class LiveDataRobolectricTest {

    private lateinit var lifecycleOwner: LifecycleOwner
    private lateinit var lifecycle: LifecycleRegistry

    @Before
    fun setup() {
        lifecycleOwner = LifecycleOwner { lifecycle }
        lifecycle = LifecycleRegistry(lifecycleOwner)
        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
    }

    @Test
    fun testLiveData_withRobolectric() {
        val liveData = MutableLiveData<String>()
        val observer = Observer<String> {}

        liveData.observe(lifecycleOwner, observer)

        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
        
        liveData.value = "Hello, Robolectric!"

        assertEquals("Hello, Robolectric!", liveData.value)

        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
    }
}