• 在 Android 上测试 Kotlin 协程


    官方文档

    https://developer.android.google.cn/kotlin/coroutines/test?hl=zh-cn
    API 是 kotlinx.coroutines.test 库的一部分。如需访问这些 API,请务必添加相应工件作为项目的测试依赖项。

    dependencies {
        testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
    }
    
    • 1
    • 2
    • 3

    在测试中调用挂起函数

    如需在测试中调用挂起函数,您必须位于协程中。由于 JUnit 测试函数本身并不是挂起函数,因此您需要在测试中调用协程构建器以启动新的协程。

    suspend fun fetchData(): String {
        delay(1000L)
        return "Hello world"
    }
    
    @Test
    fun dataShouldBeHelloWorld() = runTest {
        val data = fetchData()
        assertEquals("Hello world", data)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    TestDispatchers

    TestDispatchers 是用于测试的 CoroutineDispatcher 实现。如果要在测试期间创建新的协程,您需要使用 TestDispatchers,以使新协程的执行可预测。

    注意:新协程可直接在测试主体中创建,也可在测试中所调用的任何代码中创建(例如在测试的对象中)。
    TestDispatcher 有两种可用的实现:StandardTestDispatcher 和 UnconfinedTestDispatcher,可分别对新启动的协程执行不同的调度。两者都使用 TestCoroutineScheduler 来控制虚拟时间并管理测试中正在运行的协程。

    一个测试中只能使用一个调度器实例,且所有 TestDispatchers 应共用该调度器。如需了解如何共用调度器,请参阅注入测试调度程序。

    为了启动顶级测试协程,runTest 会创建一个 TestScope,它是 CoroutineScope 的实现,将始终使用 TestDispatcher。如果未指定,TestScope 将默认创建 StandardTestDispatcher,并将其用于运行顶级测试协程。

    StandardTestDispatcher

    @Test
    fun standardTest() = runTest {
        val userRepo = UserRepository()
    
        launch { userRepo.register("Alice") }
        launch { userRepo.register("Bob") }
    
        assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可通过多种方式让出测试协程,以让排队的协程运行。所有以下调用都可在返回之前让其他协程在测试线程上运行:

    advanceUntilIdle:在调度器上运行所有其他协程,直到队列中没有任何内容。这是一个不错的默认选择,可让所有待处理的协程运行,适用于大多数测试场景。
    advanceTimeBy:将虚拟时间提前指定时长,并运行已调度为在该虚拟时间点之前运行的所有协程。
    runCurrent:运行已调度为在当前虚拟时间运行的协程。

    UnconfinedTestDispatcher

    如果在 UnconfinedTestDispatcher 上启动新协程,系统会在当前线程上快速启动。也就是说,这些协程会立即开始运行,而不会等待其协程构建器返回。在许多情况下,这种调度行为会使测试代码更加简单,因为您无需手动让出测试线程即可让新协程运行。

    @Test
    fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) {
        val userRepo = UserRepository()
    
        launch { userRepo.register("Alice") }
        launch { userRepo.register("Bob") }
    
        assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注入测试调度程序

    设置主调度程序

    在本地单元测试中,封装 Android 界面线程的 Main 调度程序将无法使用,因为这些测试是在本地 JVM 而不是 Android 设备上执行的。如果被测试代码引用主线程,它会在单元测试期间抛出异常。

    在测试之外创建调度程序

    class Repository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ }
    
    class RepositoryTestWithRule {
        private val repository = Repository(/* What TestDispatcher? */)
    
        @get:Rule
        val mainDispatcherRule = MainDispatcherRule()
    
        @Test
        fun someRepositoryTest() = runTest {
            // Test the repository...
            // ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    创建您自己的 TestScope

    class SimpleExampleTest {
        val testScope = TestScope() // Creates a StandardTestDispatcher
    
        @Test
        fun someTest() = testScope.runTest {
            // ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注入作用域

    如果有类创建需要您在测试期间控制的协程,则可以将协程作用域注入到该类中,并在测试中将其替换为 TestScope

    class UserState(
        private val userRepository: UserRepository,
        private val scope: CoroutineScope,
    ) {
        private val _users = MutableStateFlow(emptyList<String>())
        val users: StateFlow<List<String>> = _users.asStateFlow()
    
        fun registerUser(name: String) {
            scope.launch {
                userRepository.register(name)
                _users.update { userRepository.getAllUsers() }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    class UserStateTest {
        @Test
        fun addUserTest() = runTest { // this: TestScope
            val repository = FakeUserRepository()
            val userState = UserState(repository, scope = this)
    
            userState.registerUser("Mona")
            advanceUntilIdle() // Let the coroutine complete and changes propagate
    
            assertEquals(listOf("Mona"), userState.users.value)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 相关阅读:
    基本的SELECT语句
    Redis非关系型数据库
    【OSPF宣告——network命令与多区域配置实验案例】
    jmeter,实现不写文本文件 只要写脚本,自动的给注册
    [SpringCloud] Nacos 简介
    Chapter9.3:线性系统的状态空间分析与综合(下)
    数据中心网络是什么?如何管理数据中心网络
    FPGA帧差算法实现图像识别跟踪,Verilog代码讲解全网最细,提供两套工程源码
    LLM实战(二)| 使用ChatGPT API提取文本topic
    Photoshop_00000
  • 原文地址:https://blog.csdn.net/baopengjian/article/details/133949970