Android Kotlin 协程单元测试

标签 android kotlin kotlin-coroutines

我有一个启动协程的 broadcastReceiver,我正在尝试对其进行单元测试...

广播:

class AlarmBroadcastReceiver: BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {
    Timber.d("Starting alarm from broadcast receiver")
    //inject(context) Don't worry about this, it's mocked out

    GlobalScope.launch {
        val alarm = getAlarm(intent)
        startTriggerActivity(alarm, context)
    }
}

private suspend fun getAlarm(intent: Intent?): Alarm {
    val alarmId = intent?.getIntExtra(AndroidAlarmService.ALARM_ID_KEY, -1)
    if (alarmId == null || alarmId < 0) {
        throw RuntimeException("Cannot start an alarm with an invalid ID.")
    }

    return withContext(Dispatchers.IO) {
        alarmRepository.getAlarmById(alarmId)
    }
}

下面是测试:

@Test
fun onReceive_ValidAlarm_StartsTriggerActivity() {
    val alarm = Alarm().apply { id = 100 }
    val intent: Intent = mock {
        on { getIntExtra(any(), any()) }.thenReturn(alarm.id)
    }

    whenever(alarmRepository.getAlarmById(alarm.id)).thenReturn(alarm)

    alarmBroadcastReceiver.onReceive(context, intent)

    verify(context).startActivity(any())
}

发生的事情是我正在验证的函数从未被调用过。测试在协程返回之前结束...我知道 GlobalScope 不好用,但我不确定该怎么做。

编辑 1: 如果我在 verify 之前延迟,它似乎可以工作,因为它允许协程有时间完成并返回,但是,我不想依赖延迟/ sleep 进行测试。 . 我认为解决方案是适本地引入一个范围而不是使用 GlobalScope 并在测试中控制它。 las,我不知道声明协程作用域的约定是什么。

最佳答案

我明白了,你必须使用 Unconfined调度员:

val Unconfined: CoroutineDispatcher (source)

A coroutine dispatcher that is not confined to any specific thread. It executes the initial continuation of a coroutine in the current call-frame and lets the coroutine resume in whatever thread that is used by the corresponding suspending function, without mandating any specific threading policy. Nested coroutines launched in this dispatcher form an event-loop to avoid stack overflows.

Documentation sample:

withContext(Dispatcher.Unconfined) {
   println(1)
   withContext(Dispatcher.Unconfined) { // Nested unconfined
       println(2)
   }
   println(3)
}
println("Done")

对于我的 ViewModel 测试,我将协程上下文传递给 ViewModel 构造函数,以便我可以在 Unconfined 和其他调度程序之间切换,例如Dispatchers.MainDispatchers.IO

测试的协程上下文:

@ExperimentalCoroutinesApi
class TestContextProvider : CoroutineContextProvider() {
    override val Main: CoroutineContext = Unconfined
    override val IO: CoroutineContext = Unconfined
}

实际 ViewModel 实现的协程上下文:

open class CoroutineContextProvider {
    open val Main: CoroutineContext by lazy { Dispatchers.Main }
    open val IO: CoroutineContext by lazy { Dispatchers.IO }
}

View 模型:

@OpenForTesting
class SampleViewModel @Inject constructor(
        val coroutineContextProvider: CoroutineContextProvider
) : ViewModel(), CoroutineScope {

    private val job = Job()

    override val coroutineContext: CoroutineContext = job + coroutineContextProvider.Main
    override fun onCleared() = job.cancel()

    fun fetchData() {
        launch {
            val response = withContext(coroutineContextProvider.IO) {
                repository.fetchData()
            }
        }
    }

}

更新

从协程核心版本 1.2.1 开始,您可以使用 runBlockingTest:

依赖关系:

def coroutines_version = "1.2.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"

例如:

@Test
fun `sendViewState() sends displayError`(): Unit = runBlockingTest {
    Dispatchers.setMain(Dispatchers.Unconfined)
    val apiResponse = ApiResponse.success(data)
    whenever(repository.fetchData()).thenReturn(apiResponse) 
    viewModel.viewState.observeForever(observer)
    viewModel.processData()
    verify(observer).onChanged(expectedViewStateSubmitError)
}

关于Android Kotlin 协程单元测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56531647/

相关文章:

android - 如何在 Kotlin Android 中为数据类创建构造函数?

使用 TestCoroutineDispatcher(已弃用)替代方案对 Kotlin 协程进行 Android 测试

android - Recyclerview 不调用任何适配器方法 :onBindViewHolder, onCreateViewHolder

android - 当我在android中单击ListPreference时,如何设置对话框中项目的字体大小?

使用背景图片时Android慢速滑动菜单

kotlin - @Around 方面和 Kotlin 挂起函数

android - viewModelScope 未取消

android - 将 sqlite 数据从 Android Unit 传输到 Web 服务器的最佳方法?

android - 评估项目 ':app'时发生问题。 > IntelliJ IDEA上用于Android应用程序开发的ASCII uisng kotlin

json - Moshi 忽略 Kotlin 中的字段