掌握 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)
// 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:簡單方法模擬
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 trueassertTrue(authMock.login("admin", "123456"))verify(exactly = 1) { authMock.login(any(), any()) }
}
場景 2:異常拋出模擬
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:參數捕獲與驗證
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:靈活參數匹配
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 trueval testUser = User(name = "Alice", age = 20)assertTrue(validator.isEligible(testUser))
}
四、高級技巧實戰
4.1 靜態方法與單例 Mock
場景 5:單例對象 Mock
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
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 測試
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 testProductsval viewModel = ProductViewModel(repo)viewModel.loadProducts()// 使用 Turbine 庫簡化 Flow 測試viewModel.products.test {assertEquals(emptyList(), awaitItem()) // 初始狀態assertEquals(testProducts, awaitItem())cancel()}
}
4.3 Android 平臺特殊處理
場景 8:Context 模擬
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 mockResevery { mockRes.getString(R.string.app_name) } returns "MockApp"val provider = StringProvider(mockContext)assertEquals("MockApp", provider.getAppName())
}
五、最佳實踐清單
-
分層驗證策略:
verify {service.callMethod(exact = 1) // 精確次數service.anotherMethod(atLeast = 2) // 最少調用 }
-
組合驗證:
verifyAll {service.methodA()service.methodB() }
-
智能參數捕獲:
val allParams = mutableListOf<String>() every { service.log(capture(allParams)) } just Runs
-
真實對象部分模擬:
val realService = RealService() val spy = spyk(realService)every { spy.shouldMock() } returns false
六、常見陷阱規避
陷阱 1:未清理 Mock 狀態
@After
fun tearDown() {unmockkAll() // 必須清理防止測試污染
}
陷阱 2:錯誤的作用域驗證
class OrderService {private fun internalValidate() { /* ... */ } // 私有方法無法 Mock
}// 正確做法:重構為 protected 或使用接口
結語
建議在實際項目中:
- 從簡單場景入手,逐步嘗試高級功能
- 結合 Kotlin 協程測試工具(如
runTest
) - 定期查看 MockK 官方文檔 獲取更新