Android Kotlin协程单元测试

Mit*_*tch 1 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

这是测试:

@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())
}
Run Code Online (Sandbox Code Playgroud)

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

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

Rod*_*roz 5

我知道,您将必须使用Unconfined调度程序:

val Unconfined: CoroutineDispatcher (source)

不局限于任何特定线程的协程调度程序。它在当前调用框架中执行协程的初始延续,并让协程在相应的挂起函数使用的任何线程中恢复,而无需强制执行任何特定的线程策略。在此调度程序中启动的嵌套协程形成一个事件循环,以避免堆栈溢出。

文档样本:

withContext(Dispatcher.Unconfined) {
   println(1)
   withContext(Dispatcher.Unconfined) { // Nested unconfined
       println(2)
   }
   println(3)
}
println("Done")
Run Code Online (Sandbox Code Playgroud)

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

测试的协程上下文:

@ExperimentalCoroutinesApi
class TestContextProvider : CoroutineContextProvider() {
    override val Main: CoroutineContext = Unconfined
    override val IO: CoroutineContext = Unconfined
}
Run Code Online (Sandbox Code Playgroud)

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

open class CoroutineContextProvider {
    open val Main: CoroutineContext by lazy { Dispatchers.Main }
    open val IO: CoroutineContext by lazy { Dispatchers.IO }
}
Run Code Online (Sandbox Code Playgroud)

ViewModel:

@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()
            }
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

更新资料

从协程核心版本开始,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"
Run Code Online (Sandbox Code Playgroud)

例如:

@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)
}
Run Code Online (Sandbox Code Playgroud)