Kotlin Runtest with delay()不起作用

发布于 2025-02-04 12:12:07 字数 1252 浏览 2 评论 0原文

我正在测试一个阻塞的Coroutine。这是我的生产代码:

interface Incrementer {
    fun inc()
}

class MyViewModel : Incrementer, CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.IO

    private val _number = MutableStateFlow(0)
    fun getNumber(): StateFlow<Int> = _number.asStateFlow()

    override fun inc() {
        launch(coroutineContext) {
            delay(100)
            _number.tryEmit(1)
        }
    }
}

我的测试:

class IncTest {
    @BeforeEach
    fun setup() {
        Dispatchers.setMain(StandardTestDispatcher())
    }

    @AfterEach
    fun teardown() {
        Dispatchers.resetMain()
    }

    @Test
    fun incrementOnce() = runTest {
        val viewModel = MyViewModel()

        val results = mutableListOf<Int>()
        val resultJob = viewModel.getNumber()
            .onEach(results::add)
            .launchIn(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))

        launch(StandardTestDispatcher(testScheduler)) {
            viewModel.inc()
        }.join()

        assertEquals(listOf(0, 1), results)
        resultJob.cancel()
    }
}

如何测试我的 inc()功能? (将界面刻在石头上,所以我不能将 inc()变成悬浮函数。)

I am testing a coroutine that blocks. Here is my production code:

interface Incrementer {
    fun inc()
}

class MyViewModel : Incrementer, CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.IO

    private val _number = MutableStateFlow(0)
    fun getNumber(): StateFlow<Int> = _number.asStateFlow()

    override fun inc() {
        launch(coroutineContext) {
            delay(100)
            _number.tryEmit(1)
        }
    }
}

And my test:

class IncTest {
    @BeforeEach
    fun setup() {
        Dispatchers.setMain(StandardTestDispatcher())
    }

    @AfterEach
    fun teardown() {
        Dispatchers.resetMain()
    }

    @Test
    fun incrementOnce() = runTest {
        val viewModel = MyViewModel()

        val results = mutableListOf<Int>()
        val resultJob = viewModel.getNumber()
            .onEach(results::add)
            .launchIn(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))

        launch(StandardTestDispatcher(testScheduler)) {
            viewModel.inc()
        }.join()

        assertEquals(listOf(0, 1), results)
        resultJob.cancel()
    }
}

How would I go about testing my inc() function? (The interface is carved in stone, so I can't turn inc() into a suspend function.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

话少心凉 2025-02-11 12:12:07

这里有两个问题:

  1. 您想在Coroutine中等待viewModel.inc()在内部启动的工作。
  2. 理想情况下,应在测试期间快速延迟100ms延迟,以便实际上不需要100ms才能执行。

让我们从问题2开始:为此,您需要能够修改myViewModel(但不是inc),然后更改类,以便不使用硬编码dispatchers.io,它接收coroutinecontext作为参数。这样,您可以在测试中传递testDisPatcher,这将利用虚拟时间快速向前延迟。您可以在“ noreferrer”>注射testdispatchers Android文档。

class MyViewModel(coroutineContext: CoroutineContext) : Incrementer {
    private val scope = CoroutineScope(coroutineContext)

    private val _number = MutableStateFlow(0)
    fun getNumber(): StateFlow<Int> = _number.asStateFlow()

    override fun inc() {
        scope.launch {
            delay(100)
            _number.tryEmit(1)
        }
    }
}

在这里,我还完成了一些较小的清理:

  • 制作myViewModel包含coroutinesCope而不是实现接口,即正式推荐的练习
  • 删除了coroutinecontext参数传递给pluench> punains, '在这种情况下做任何事情 - 无论如何,相同的上下文都在范围中,因此它已经用于

问题1,等待工作完成,您有一些选择:

  • 如果您通过了testDisPatcher,您可以使用Advanceununtilidle之类的测试方法手动手动推进inc内部创建的coroutine。这不是理想的选择,因为您非常依赖实施细节,这是您在生产中无法做的事情。但是,如果您无法使用下面的更好的解决方案,它将起作用。

      viewmodel.inc()
    AdvanceUnuntilidle()//返回所有待处理的巡回赛完成
     
  • 适当的解决方案将用于inc,可以让其呼叫者何时完成工作。您可以使其成为一种悬浮方法,而不是内部启动新的Coroutine,但您表示无法修改使其暂停的方法。替代方案(如果您能够进行此更改)是使用inc使用async构建器创建新的Coroutine,返回延迟创建的对象,然后在呼叫网站上

    -ing。

      Override fun inc():延迟&lt; unit&gt; {
        scope.async {
            延迟(100)
            _number.tryemit(1)
        }
    }
    
    //在测试中...
    viewModel.inc()。等待()
     
  • 如果您无法修改方法或类,则无法避免delay> delay()调用导致真正的100ms延迟。在这种情况下,您可以强迫测试在继续之前等待此时间。 runtest内的常规delay>将通过使用testDisPatcher为其创建的coroutine快速抛光,但您可以逃脱其中一种解决方案:

      // delay> // delay()
    viewModel.inc()
    使用context(dispatchers.default){delay(100)}
    
    //使用阻塞睡眠
    viewModel.inc()
    线程(100)
     

对于有关测试代码的一些最终注释:

  • 由于您正在执行dispatchers.setmain,因此您无需将testschedscheduler传递到<<代码> testDisPatchers 您创建。他们将从main中获取调度程序,如果他们在其中找到testDisPatcher,如上所述在其文档中
  • 您可以简单地传递this,而不是创建新的范围以传递到lainingin,它是runtest的接收器,它指向a <代码> TestScope 。

There are two problems here:

  1. You want to wait for the work done in the coroutine that viewModel.inc() launches internally.
  2. Ideally, the 100ms delay should be fast-forwarded during tests so that it doesn't actually take 100ms to execute.

Let's start with problem #2 first: for this, you need to be able to modify MyViewModel (but not inc), and change the class so that instead of using a hardcoded Dispatchers.IO, it receives a CoroutineContext as a parameter. With this, you could pass in a TestDispatcher in tests, which would use virtual time to fast-forward the delay. You can see this pattern described in the Injecting TestDispatchers section of the Android docs.

class MyViewModel(coroutineContext: CoroutineContext) : Incrementer {
    private val scope = CoroutineScope(coroutineContext)

    private val _number = MutableStateFlow(0)
    fun getNumber(): StateFlow<Int> = _number.asStateFlow()

    override fun inc() {
        scope.launch {
            delay(100)
            _number.tryEmit(1)
        }
    }
}

Here, I've also done some minor cleanup:

  • Made MyViewModel contain a CoroutineScope instead of implementing the interface, which is an officially recommended practice
  • Removed the coroutineContext parameter passed to launch, as it doesn't do anything in this case - the same context is in the scope anyway, so it'll already be used

For problem #1, waiting for work to complete, you have a few options:

  • If you've passed in a TestDispatcher, you can manually advance the coroutine created inside inc using testing methods like advanceUntilIdle. This is not ideal, because you're relying on implementation details a lot, and it's something you couldn't do in production. But it'll work if you can't use the nicer solution below.

    viewModel.inc()
    advanceUntilIdle() // Returns when all pending coroutines are done
    
  • The proper solution would be for inc to let its callers know when it's done performing its work. You could make it a suspending method instead of launching a new coroutine internally, but you stated that you can't modify the method to make it suspending. An alternative - if you're able to make this change - would be to create the new coroutine in inc using the async builder, returning the Deferred object that that creates, and then await()-ing at the call site.

    override fun inc(): Deferred<Unit> {
        scope.async {
            delay(100)
            _number.tryEmit(1)
        }
    }
    
    // In the test...
    viewModel.inc().await()
    
  • If you're not able to modify either the method or the class, there's no way to avoid the delay() call causing a real 100ms delay. In this case, you can force your test to wait for that amount of time before proceeding. A regular delay() within runTest would be fast-forwarded thanks to it using a TestDispatcher for the coroutine it creates, but you can get away with one of these solutions:

    // delay() on a different dispatcher
    viewModel.inc()
    withContext(Dispatchers.Default) { delay(100) }
    
    // Use blocking sleep
    viewModel.inc()
    Thread.sleep(100)
    

For some final notes about the test code:

  • Since you're doing Dispatchers.setMain, you don't need to pass in testScheduler into the TestDispatchers you create. They'll grab the scheduler from Main automatically if they find a TestDispatcher there, as described in its docs.
  • Instead of creating a new scope to pass in to launchIn, you could simply pass in this, the receiver of runTest, which points to a TestScope.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文