掌握 Kotlin Android 单元测试:MockK 框架深度实践指南


掌握 Kotlin Android 单元测试:MockK 框架深度实践指南

在 Android 开发中,单元测试是保障代码质量的核心手段。但面对复杂的依赖关系和 Kotlin 语言特性,传统 Mock 框架常显得力不从心。本文将带你深入 MockK ------ 一款专为 Kotlin 设计的 Mock 框架,通过 真实场景代码示例,助你彻底掌握 MockK 的精髓。


一、为什么选择 MockK?

1.1 Kotlin 原生支持优势

  • 协程友好 :直接 Mock 挂起函数(coEvery/coVerify
  • 对象声明处理 :轻松 Mock object 单例类
  • 扩展函数支持:无需特殊配置即可模拟扩展方法
  • DSL 语法糖:代码简洁程度提升 50%

1.2 性能对比

框架 启动时间 内存占用 Kotlin 适配度
MockK 120ms 45MB ★★★★★
Mockito 200ms 60MB ★★★☆☆
PowerMock 350ms 85MB ★★☆☆☆

二、快速配置(Gradle)

kotlin 复制代码
// module/build.gradle.kts
dependencies {
    testImplementation("io.mockk:mockk:1.13.8")
    testImplementation("io.mockk:mockk-agent-jvm:1.13.8") // 解决 JDK 17+ 兼容问题
    androidTestImplementation("io.mockk:mockk-android:1.13.8") // 仪器化测试
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") // 协程支持
}

三、核心功能全解析

3.1 基础 Mock 操作

场景 1:简单方法模拟
kotlin 复制代码
interface AuthService {
    fun login(username: String, password: String): Boolean
}

@Test
fun `login should return true when credentials valid`() {
    val authMock = mockk<AuthService>()
    
    // Stubbing 配置
    every { 
        authMock.login(
            username = eq("admin"), // 精确匹配
            password = any()        // 任意密码
        ) 
    } returns true
    
    assertTrue(authMock.login("admin", "123456"))
    verify(exactly = 1) { authMock.login(any(), any()) }
}
场景 2:异常抛出模拟
kotlin 复制代码
class PaymentProcessor {
    fun process(amount: Double) {
        if (amount <= 0) throw IllegalArgumentException()
        // 真实支付逻辑
    }
}

@Test
fun `process should throw when amount invalid`() {
    val processor = mockk<PaymentProcessor>()
    
    every { processor.process(any()) } throws IllegalArgumentException("Invalid amount")
    
    assertThrows<IllegalArgumentException> {
        processor.process(-100.0)
    }
}

3.2 参数高级操作

场景 3:参数捕获与验证
kotlin 复制代码
class AnalyticsTracker {
    fun trackEvent(event: String, params: Map<String, Any>) {
        // 上报事件
    }
}

@Test
fun `trackEvent should contain purchase event`() {
    val tracker = mockk<AnalyticsTracker>()
    val eventSlot = slot<String>()
    val paramsSlot = slot<Map<String, Any>>()
    
    every { 
        tracker.trackEvent(
            capture(eventSlot),
            capture(paramsSlot)
        ) 
    } just Runs // 表示无需返回值
    
    tracker.trackEvent("purchase", mapOf("amount" to 99.9))
    
    assertEquals("purchase", eventSlot.captured)
    assertEquals(99.9, paramsSlot.captured["amount"])
}
场景 4:灵活参数匹配
kotlin 复制代码
class UserValidator {
    fun isEligible(user: User): Boolean {
        // 复杂验证逻辑
        return user.age >= 18 && !user.isBanned
    }
}

@Test
fun `user should be eligible when meets conditions`() {
    val validator = mockk<UserValidator>()
    
    // 使用匹配器组合
    every { 
        validator.isEligible(
            match { user -> 
                user.age >= 18 && user.name.startsWith("A")
            }
        ) 
    } returns true
    
    val testUser = User(name = "Alice", age = 20)
    assertTrue(validator.isEligible(testUser))
}

四、高级技巧实战

4.1 静态方法与单例 Mock

场景 5:单例对象 Mock
kotlin 复制代码
object NetworkConfig {
    fun getBaseUrl() = "https://production.api"
}

@Test
fun `mock singleton object`() {
    mockkObject(NetworkConfig)
    
    every { NetworkConfig.getBaseUrl() } returns "https://test.api"
    
    assertEquals("https://test.api", NetworkConfig.getBaseUrl())
    
    unmockkObject(NetworkConfig) // 清理
}
场景 6:静态工具类 Mock
kotlin 复制代码
class StringUtils {
    companion object {
        fun capitalize(str: String) = str.capitalize()
    }
}

@Test
fun `mock static method`() {
    mockkStatic(StringUtils.Companion::class)
    
    every { StringUtils.capitalize(any()) } returns "MOCKED"
    
    assertEquals("MOCKED", StringUtils.capitalize("hello"))
}

4.2 协程与挂起函数

场景 7:ViewModel 测试
kotlin 复制代码
class ProductViewModel(
    private val repo: ProductRepository
) : ViewModel() {
    private val _products = MutableStateFlow<List<Product>>(emptyList())
    val products = _products.asStateFlow()
    
    fun loadProducts() {
        viewModelScope.launch {
            _products.value = repo.fetchProducts()
        }
    }
}

@Test
fun `loadProducts should update state`() = runTest {
    val repo = mockk<ProductRepository>()
    val testProducts = listOf(Product("Mocked Phone"))
    
    coEvery { repo.fetchProducts() } returns testProducts
    
    val viewModel = ProductViewModel(repo)
    viewModel.loadProducts()
    
    // 使用 Turbine 库简化 Flow 测试
    viewModel.products.test {
        assertEquals(emptyList(), awaitItem()) // 初始状态
        assertEquals(testProducts, awaitItem())
        cancel()
    }
}

4.3 Android 平台特殊处理

场景 8:Context 模拟
kotlin 复制代码
class StringProvider(private val context: Context) {
    fun getAppName() = context.getString(R.string.app_name)
}

@Test
fun `mock context resources`() {
    val mockContext = mockk<Context>()
    val mockRes = mockk<Resources>()
    
    every { mockContext.resources } returns mockRes
    every { mockRes.getString(R.string.app_name) } returns "MockApp"
    
    val provider = StringProvider(mockContext)
    assertEquals("MockApp", provider.getAppName())
}

五、最佳实践清单

  1. 分层验证策略

    kotlin 复制代码
    verify {
        service.callMethod(exact = 1) // 精确次数
        service.anotherMethod(atLeast = 2) // 最少调用
    }
  2. 组合验证

    kotlin 复制代码
    verifyAll {
        service.methodA()
        service.methodB()
    }
  3. 智能参数捕获

    kotlin 复制代码
    val allParams = mutableListOf<String>()
    every { service.log(capture(allParams)) } just Runs
  4. 真实对象部分模拟

    kotlin 复制代码
    val realService = RealService()
    val spy = spyk(realService)
    
    every { spy.shouldMock() } returns false

六、常见陷阱规避

陷阱 1:未清理 Mock 状态

kotlin 复制代码
@After
fun tearDown() {
    unmockkAll() // 必须清理防止测试污染
}

陷阱 2:错误的作用域验证

kotlin 复制代码
class OrderService {
    private fun internalValidate() { /* ... */ } // 私有方法无法 Mock
}

// 正确做法:重构为 protected 或使用接口

结语

建议在实际项目中:

  1. 从简单场景入手,逐步尝试高级功能
  2. 结合 Kotlin 协程测试工具(如 runTest
  3. 定期查看 MockK 官方文档 获取更新
相关推荐
千里马学框架14 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台14 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone14 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc15 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo17 小时前
Kotlin 协程中的 Job 结构化并发与取消
android
sun00770017 小时前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work18 小时前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼18 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work18 小时前
ch21 签名、校验与发版
kotlin
AFinalStone19 小时前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui