掌握 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 官方文档 获取更新
相关推荐
田一一一3 小时前
Android framework 中间件开发(三)
android·中间件·framework·jni
田一一一7 小时前
Android framework 中间件开发(二)
android·中间件·framework
追随远方8 小时前
FFmpeg在Android开发中的核心价值是什么?
android·ffmpeg
神探阿航9 小时前
HNUST湖南科技大学-安卓Android期中复习
android·安卓·hnust
千里马-horse11 小时前
android vlc播放rtsp
android·media·rtsp·mediaplayer·vlc
難釋懷11 小时前
Android开发-文本输入
android·gitee
志存高远6613 小时前
(面试)Android各版本新特性
android
IT从业者张某某13 小时前
信奥赛-刷题笔记-队列篇-T3-P3662Why Did the Cow Cross the Road II S
android·笔记
未来之窗软件服务13 小时前
Cacti 未经身份验证SQL注入漏洞
android·数据库·sql·服务器安全