Kotlin Multiplatform跨多模块共享@BeforeTest与@AfterTest测试逻辑的方案咨询
我之前在做KMM多模块项目时也碰到过完全一样的问题,一开始也是想把测试逻辑塞到commonMain里,结果测试运行器根本不买账。其实问题出在两个地方:一是你把测试代码放错了源集,二是kotlin.test的注解只有在测试源集里才会被测试运行器识别。下面给你梳理一套靠谱的实现方案,包括具体步骤和最佳实践:
一、先纠正一个核心误区:别把测试逻辑放在commonMain里!
commonMain是用来放生产环境代码的,测试相关的工具类、钩子逻辑属于测试范围,放在这里不仅会污染生产代码的依赖(比如引入测试库到生产包),更关键的是测试运行器默认只会扫描测试源集(commonTest、androidTest等)里的注解,完全不会处理commonMain中的测试注解——这就是你之前测试跑不起来的根本原因。
正确的做法是:新建一个专门的共享测试工具模块,把所有跨模块复用的测试逻辑都放在这个模块的commonTest源集里。
二、具体实现步骤
1. 新建共享测试工具模块
创建一个KMM模块(比如命名为shared-test-utils),这个模块不需要保留生产源集(commonMain、androidMain等),只需要测试相关的源集即可。
在它的build.gradle.kts里配置测试依赖:
plugins { id("org.jetbrains.kotlin.multiplatform") } kotlin { jvm() ios() // 其他你需要的平台... sourceSets { val commonTest by getting { dependencies { implementation(kotlin("test")) // 如果用第三方测试库(比如MockK、Kotest),也在这里加依赖 // implementation("io.mockk:mockk-common:1.13.8") } } // 各个平台的test源集如果需要特定逻辑,可以单独配置 val jvmTest by getting { dependencies { implementation(kotlin("test-junit")) } } } }
2. 定义共享测试钩子
在shared-test-utils的commonTest源集下,创建一个基类或者单例类来封装共享的@BeforeTest和@AfterTest逻辑:
方式一:用开放基类(推荐,方便子模块扩展)
import kotlin.test.AfterTest import kotlin.test.BeforeTest open class BaseSharedTest { @BeforeTest open fun globalSetup() { // 这里写通用初始化逻辑:比如初始化日志、配置测试用DI、清理测试数据等 println("[Shared] Running setup before test...") } @AfterTest open fun globalTeardown() { // 通用清理逻辑:比如关闭数据库连接、清空缓存等 println("[Shared] Running teardown after test...") } }
方式二:用单例类(适合完全不需要扩展的固定逻辑)
import kotlin.test.AfterTest import kotlin.test.BeforeTest object SharedTestHooks { @BeforeTest fun setup() { // 通用前置逻辑 } @AfterTest fun teardown() { // 通用后置逻辑 } }
3. 在业务模块中引入共享测试模块
在需要复用测试逻辑的业务模块的build.gradle.kts里,给commonTest添加对shared-test-utils的依赖:
kotlin { sourceSets { val commonTest by getting { dependencies { implementation(project(":shared-test-utils")) } } } }
4. 在业务模块的测试中使用共享逻辑
如果用的是基类方式,直接让测试类继承BaseSharedTest即可,测试运行器会自动执行父类的@BeforeTest和@AfterTest方法:
// 业务模块中的测试类 import kotlin.test.Test class UserRepositoryTest : BaseSharedTest() { @Test fun `should fetch user data successfully`() { // 你的测试逻辑... // 执行前会先跑BaseSharedTest的globalSetup,执行后跑globalTeardown } }
如果用的是单例类方式,需要在测试类中实例化它(不过这种方式不如基类直观,更推荐基类):
class OrderServiceTest { private val hooks = SharedTestHooks @Test fun `should create order`() { // 测试逻辑 } }
三、进阶:用第三方测试框架实现更灵活的共享逻辑
如果你的项目用的是Kotest、Spek这类功能更丰富的测试框架,可以用它们的监听器/扩展机制来实现共享逻辑,比原生kotlin.test的注解更灵活:
比如用Kotest的TestListener:
import io.kotest.core.listeners.TestListener import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult class GlobalTestListener : TestListener { override suspend fun beforeTest(testCase: TestCase) { // 通用前置逻辑 } override suspend fun afterTest(testCase: TestCase, result: TestResult) { // 通用后置逻辑 } }
然后在业务模块的测试类中注册这个监听器:
import io.kotest.core.spec.style.FunSpec class PaymentTest : FunSpec({ listener(GlobalTestListener) test("should process payment") { // 测试逻辑 } })
四、最佳实践
- 保持共享测试逻辑的通用性:只放跨模块都需要的通用操作(比如日志初始化、DI配置、测试数据清理),别把业务相关的测试逻辑塞进来,否则会导致共享模块变得臃肿难维护。
- 平台特定逻辑单独处理:如果某个平台需要特殊的测试初始化(比如Android需要初始化Context),可以在共享模块的对应平台test源集里扩展基类,比如在
androidTest里重写setup方法添加Android特定逻辑。 - 避免过度封装:如果某个模块只需要少量特殊测试逻辑,直接在该模块的测试类里写即可,不要强行塞到共享模块里。
- 统一测试依赖版本:可以在项目根目录的
build.gradle.kts里用dependencyResolutionManagement统一管理测试库的版本,避免各个模块版本不一致。
内容来源于stack exchange

