简体   繁体   English

Android Kotlin协程单元测试

[英]Android Kotlin Coroutine UnitTesting

I've got a broadcastReceiver that starts a coroutine and I am trying to unit test that... 我有一个开始启动协程的broadcastReceiver,我正在尝试对该单元进行测试...

The broadcast: 广播:

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

And here's the test: 这是测试:

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

What's happening is that the function I'm verifying is never being called. 发生的事情是我正在验证的函数从未被调用。 The test ends before the coroutine returns... I'm aware that GlobalScope is bad to use, but I'm not sure how else to do it. 测试在协程返回之前结束...我知道GlobalScope不好用,但是我不确定还有其他方法。

EDIT 1: If I put a delay before the verify , it seems to work, as it allows time for the coroutine to finish and return, however, I don't want to have test relying on delay/sleep... I think the solution is to properly introduce a scope instead of using GlobalScope and control that in the test. 编辑1:如果我在verify之前放置了一个延迟,它似乎可以正常工作,因为它允许协程完成并返回的时间,但是,我不想依赖延迟/睡眠进行测试...我认为解决方案是正确引入一个范围,而不是使用GlobalScope并在测试中进行控制。 Alas, I have no clue what is the convention for declaring coroutine scopes. las,我不知道声明协程范围的约定是什么。

I see, You will have to use an Unconfined dispatcher: 我知道,您将必须使用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") 

For my ViewModel tests, I pass a coroutine context to the ViewModel constructor so that I can switch between Unconfined and other dispatchers eg Dispatchers.Main and Dispatchers.IO . 对于我的ViewModel测试,我将协程上下文传递给ViewModel构造函数,以便可以在Unconfined和其他分派器(例如Dispatchers.MainDispatchers.IO之间切换。

Coroutine context for tests: 测试的协程上下文:

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

Coroutine context for the actual ViewModel implementation: 实际ViewModel实现的协程上下文:

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

ViewModel: 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()
            }
        }
    }

}

Update 更新资料

As of coroutine-core version 1.2.1 you can use runBlockingTest : 从协程核心1.2.1版开始,您可以使用runBlockingTest

Dependencies: 依存关系:

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

eg: 例如:

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

Yeah as Rodrigo Queiroz mentioned, run blocking will solve the issue. 是的,正如Rodrigo Queiroz提到的那样,运行阻塞将解决该问题。

@Test
fun onReceive_ValidAlarm_StartsTriggerActivity() = runBlockingTest {
    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())
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM