简体   繁体   English

Android Kotlin - 单元测试延迟协程,在延迟完成时调用 lambda 操作

[英]Android Kotlin - Unit test a delayed coroutine that invokes a lambda action when delay is finished

Ive been struggling with this for quite some time now, perhaps someone could help... I have this function in my class under test:我已经为此苦苦挣扎了很长一段时间,也许有人可以提供帮助......我在测试中的 class 中有这个 function:

fun launchForegroundTimer(context: Context) {
        helper.log("AppRate", "[$TAG] Launching foreground count down [10 seconds]")
        timerJob = helper.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), context, this::showGoodPopupIfAllowed)
}

So in that function, I first write to some log and then I call a coroutine function that expects a Dispatcher param, how long to wait before running the action, Any object that I would like to pass on to the action and the actual action function that is invoked when time has passed. So in that function, I first write to some log and then I call a coroutine function that expects a Dispatcher param, how long to wait before running the action, Any object that I would like to pass on to the action and the actual action function时间过去后调用。

So in this case, the this::showGoodPopupIfAllowed which is a private method in the class, gets called when the 10,000 ms have passed.所以在这种情况下, this::showGoodPopupIfAllowed是 class 中的私有方法,在 10,000 毫秒过去后被调用。 Here is that function:这是function:

private fun showGoodPopupIfAllowed(context: Context?) {
        if (isAllowedToShowAppRate()) {
            showGoodPopup(context)
        }
}

In that first if, there are a bunch of checks that occur before I can call showGoodPopup(context)在第一个 if 中,在我调用showGoodPopup(context)之前会发生一堆检查

Now, here is the helper.launchActionInMillisWithBundle function:现在,这里是helper.launchActionInMillisWithBundle function:

fun <T> launchActionInMillisWithBundle(dispatcher: CoroutineContext, inMillis: Long, bundle: T, action: (T) -> Unit): Job = CoroutineScope(dispatcher).launchInMillisWithBundle(inMillis, bundle, action)

And here is the actual CoroutineScope extension function:这是实际的 CoroutineScope 扩展 function:

fun <T> CoroutineScope.launchInMillisWithBundle(inMillisFromNow: Long, bundle: T, action: (T) -> Unit) = this.launch {
    delay(inMillisFromNow)
    action(bundle)
}

What I am trying to achieve is a UnitTest that calls the launchForegroundTimer function, calls the helper function with the appropriate arguments and also continue through and call that lambda showGoodPopupIfAllowed function where I can also provide mocked behaviour to all the IF statments that occur in isAllowedToShowAppRate . What I am trying to achieve is a UnitTest that calls the launchForegroundTimer function, calls the helper function with the appropriate arguments and also continue through and call that lambda showGoodPopupIfAllowed function where I can also provide mocked behaviour to all the IF statments that occur in isAllowedToShowAppRate .

Currently my test stops right after the launchActionInMillisWithBundle is called and the test just ends.目前,我的测试在调用launchActionInMillisWithBundle后立即停止并且测试刚刚结束。 I assume there is no real call to any coroutine because I am mocking the helper class... not sure how to continue here.我假设没有对任何协程的真正调用,因为我是 mocking helper class ......不知道如何在这里继续。

I read a few interesting articles but none seems to resolve such state.我读了一些有趣的文章,但似乎没有一篇能解决这样的 state。 My current test function looks like this:我当前的测试 function 看起来像这样:

    private val appRaterManagerHelperMock = mockkClass(AppRaterManagerHelper::class)
    private val timerJobMock = mockkClass(Job::class)
    private val contextMock = mockkClass(Context::class)

    @Test
    fun `launch foreground timer`() {
        every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, any()) } returns timerJobMock
        val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
        appRaterManager.launchForegroundTimer(contextMock)

        verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
    }

I'm using mockk as my Mocking lib.我使用 mockk 作为我的 Mocking 库。 AppRaterManager is the class under test AppRaterManager是被测的class

I'd like to also mention that, in theory I could have moved the coroutine invocation outside the class under test.我还想提一下,理论上我可以将协程调用移到正在测试的 class 之外。 So an external class like activity.onResume() could launch some sort of countdown and then call directly a function that checks showGoodPopupIfAllowed() .因此,像activity.onResume()这样的外部 class 可以启动某种倒计时,然后直接调用 function 来检查showGoodPopupIfAllowed() But currently, please assume that I do not have any way to change the calling code so the timer and coroutine should remain in the class under test domain.但是目前,请假设我没有任何方法可以更改调用代码,因此计时器和协程应该保留在测试域中的 class 中。

Thank you!谢谢!

Alright, I read a bit deeper into capturing/answers over at https://mockk.io/#capturing and saw there is a capture function.好吧,我在https://mockk.io/#capturing上更深入地了解了捕获/答案,发现有一个capture function。 So I captured the lambda function in a slot which enables me invoke the lambda and then the actual code continues in the class under test. So I captured the lambda function in a slot which enables me invoke the lambda and then the actual code continues in the class under test. I can mock the rest of the behavior from there.我可以从那里模拟行为的 rest。

Here is my test function for this case (for anyone who gets stuck):这是我针对这种情况的测试 function (适用于任何被卡住的人):

    @Test
    fun `launch foreground timer, not participating, not showing good popup`() {
        val slot = slot<(Context) -> Unit>()
        every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, capture(slot)) } answers {
            slot.captured.invoke(contextMock)
            timerJobMock
        }

        every { appRaterManagerHelperMock.isParticipating() } returns false

        val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
        appRaterManager.launchForegroundTimer(contextMock)

        verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
        verify(exactly = 1) { appRaterManagerHelperMock.isParticipating() }
        verify(exactly = 0) { appRaterManagerHelperMock.showGoodPopup(contextMock, appRaterManager) }
    }

So what's left now is how to test the coroutine actually invokes the lambda after the provided delay time is up.所以现在剩下的就是如何测试协程在提供的延迟时间结束后实际调用 lambda。

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

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