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:新建独立公共测试模块(行业通用最佳实践,推荐)
这是多模块项目共享测试代码的标准实现,完全不会污染生产代码依赖:
- 新建一个独立的Android Library模块,命名为
test-utils(命名可自定义),专门存放所有复用的测试工具类、测试规则、测试Fake实现 - 将
MainCoroutineRule以及其他需要跨模块复用的测试代码,放到test-utils模块的main源码目录下 - 在
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" } - 在需要写单元测试的业务模块中,仅需通过
testImplementation依赖test-utils模块即可正常使用MainCoroutineRule:// 业务模块build.gradle依赖配置 dependencies { // 常规生产代码依赖省略 testImplementation project(':test-utils') }
该方案下test-utils的代码仅会在单元测试执行阶段被打包引入,完全不会进入正式生产APK,也不需要给生产代码配置任何测试相关依赖。
方案2:配置公共base模块暴露测试源码(备选,维护成本高)
如果不想新增独立模块,可以通过Gradle配置将base模块的test源码对外暴露,供其他模块测试使用:
- 在base模块的
build.gradle中添加如下配置,将test目录源码打包为可对外依赖的产物:android { // 常规配置省略 } configurations { testOutput } task testJar(type: Jar) { from sourceSets.test.output archiveClassifier.set('test') } artifacts { testOutput testJar } - 在其他业务模块中依赖base模块的test输出产物:
dependencies { testImplementation project(path: ':base', configuration: 'testOutput') }
该方案配置繁琐,后续如果需要共享instrumented测试(androidTest)代码还需要额外添加配置,灵活性远低于独立测试模块方案,不推荐长期使用。
注意:不要尝试将测试类放到公共业务模块的main目录,哪怕通过
debugImplementation等变体配置隔离,也会增加依赖关系的复杂度,提升后续问题排查成本。
内容的提问来源于stack exchange,提问作者Egemen Hamutçu
相关产品推荐
相关产品推荐

