You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android单元测试如何跨模块复用公共协程测试规则

问题描述

我编写了一个实现TestCoroutineScope接口、继承TestWatcher的协程测试规则类,代码如下:

@ExperimentalCoroutinesApi
class MainCoroutineRule(private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) :
TestWatcher(),
TestCoroutineScope by TestCoroutineScope(dispatcher) {
    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(dispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        cleanupTestCoroutines()
        Dispatchers.resetMain()
    }
}

该类用于为使用Kotlin协程的ViewModelTest提供主线程Looper支持,使用示例如下:

@RunWith(JUnit4::class)
class BlaViewModelTest {

    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    @ExperimentalCoroutinesApi
    @get:Rule
    val coroutineRule = MainCoroutineRule()

    @MockK
    lateinit var blaUseCase: BlaUseCase

    private lateinit var blaViewModel: BlaViewModel

    @Before
    fun setup() {
        MockKAnnotations.init(this)
        blaViewModel = BlaViewModel(blaUseCase)
    }

    @Test
    fun testBla_Positive() {
        coEvery {
            blaUseCase.execute(any()).await()
        } returns Resource.Success(Bla("data"))

        blaViewModel.blaLiveData.observeForever {}
        blaViewModel.bla()

        assert(blaViewModel.blaLiveData.value != null)
        assert(blaViewModel.blaLiveData.value is Resource.Success)
        assert((blaViewModel.blaLiveData.value as? Resource.Success)?.value?.data == "data")
    }
}

目前遇到的问题:

  • MainCoroutineRule仅能在BlaViewModelTest所在模块的test目录下访问
  • 将MainCoroutineRule移动到公共模块(例如名为base的模块)的test目录下时,编译阶段不报错,但测试运行时BlaViewModelTest无法访问MainCoroutineRule,测试执行失败
  • 尝试过将MainCoroutineRule移动到base模块的main主源码包下,但该方案需要在正式项目代码中引入测试库依赖,实现不合理
  • 不希望在所有模块中重复编写MainCoroutineRule,需要可从统一公共源访问该测试规则的可行方案

解决方案

方案1:新建独立公共测试模块(行业通用最佳实践,推荐)

这是多模块项目共享测试代码的标准实现,完全不会污染生产代码依赖:

  1. 新建一个独立的Android Library模块,命名为test-utils(命名可自定义),专门存放所有复用的测试工具类、测试规则、测试Fake实现
  2. 将MainCoroutineRule以及其他需要跨模块复用的测试代码,放到test-utils模块的main源码目录下
  3. 在test-utils模块的build.gradle中,将所有测试相关依赖声明为api,保证依赖该模块的业务模块可以直接拿到相关测试依赖,无需重复声明:
    // test-utils模块build.gradle示例
    plugins {
        id 'com.android.library'
        id 'org.jetbrains.kotlin.android'
    }
    
    android {
        // 常规SDK、编译配置和普通library模块一致,省略
    }
    
    dependencies {
        // 测试相关依赖用api声明,自动传递给依赖该模块的业务方
        api "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
        api "junit:junit:$junitVersion"
        api "androidx.arch.core:core-testing:$archTestingVersion"
        api "io.mockk:mockk:$mockkVersion"
    }
    
  4. 在需要写单元测试的业务模块中,仅需通过testImplementation依赖test-utils模块即可正常使用MainCoroutineRule:
    // 业务模块build.gradle依赖配置
    dependencies {
        // 常规生产代码依赖省略
        testImplementation project(':test-utils')
    }
    

该方案下test-utils的代码仅会在单元测试执行阶段被打包引入,完全不会进入正式生产APK,也不需要给生产代码配置任何测试相关依赖。

方案2:配置公共base模块暴露测试源码(备选,维护成本高)

如果不想新增独立模块,可以通过Gradle配置将base模块的test源码对外暴露,供其他模块测试使用:

  1. 在base模块的build.gradle中添加如下配置,将test目录源码打包为可对外依赖的产物:
    android {
        // 常规配置省略
    }
    
    configurations {
        testOutput
    }
    
    task testJar(type: Jar) {
        from sourceSets.test.output
        archiveClassifier.set('test')
    }
    
    artifacts {
        testOutput testJar
    }
    
  2. 在其他业务模块中依赖base模块的test输出产物:
    dependencies {
        testImplementation project(path: ':base', configuration: 'testOutput')
    }
    

该方案配置繁琐,后续如果需要共享instrumented测试(androidTest)代码还需要额外添加配置,灵活性远低于独立测试模块方案,不推荐长期使用。

注意:不要尝试将测试类放到公共业务模块的main目录,哪怕通过debugImplementation等变体配置隔离,也会增加依赖关系的复杂度,提升后续问题排查成本。


内容的提问来源于stack exchange,提问作者Egemen Hamutçu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.10 16:15:45