Compose 框架完全可以对 ViewModel 独立编写测试用例
实际上,ViewModel 的测试与 UI 框架无关,它是纯粹的 Kotlin 类,可以在 JVM 上独立运行,不需要 Android 环境。
┌─────────────────────────────────────────────────────────┐
│ 测试分层架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ UI 测试 (androidTest) │ │
│ │ - 需要模拟器/真机 │ │
│ │ - 测试 UI 交互 │ │
│ │ - 依赖 Compose UI 组件 │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ ViewModel 测试 (test) ✅ 独立 │ │
│ │ - 不需要模拟器 │ │
│ │ - 测试业务逻辑 │ │
│ │ - 不依赖 Compose │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 数据层测试 (test) │ │
│ │ - Repository、DataSource 测试 │ │
│ │ - 完全独立 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
AbsListViewModelTest.kt
package com.younghare.qqhelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
//说明:1:advanceTimeBy(1500) 可能无法正确触发协程执行 使用 advanceUntilIdle() 代替 advanceTimeBy()
// 2:需要导入kotlinx-coroutines-test
@OptIn(ExperimentalCoroutinesApi::class)
class AbsListViewModelTest {
private lateinit var viewModel: AbsListViewModel
private val testDispatcher = StandardTestDispatcher()
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
viewModel = AbsListViewModel()
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
// ============================================
// 测试1:初始加载数据
// ============================================
@Test
fun testInitialLoad() = runTest {
// ✅ 使用 advanceUntilIdle() 等待所有协程完成
testDispatcher.scheduler.advanceUntilIdle()
val dataList = viewModel.dataList.value
assertEquals("初始应该有10条数据", 10, dataList.size)
assertEquals("第一条数据应该是'项目 1'", "项目 1", dataList[0])
assertEquals("最后一条数据应该是'项目 10'", "项目 10", dataList[9])
assertFalse("初始加载应该完成", viewModel.initialLoading.value)
}
// ============================================
// 测试2:添加数据
// ============================================
@Test
fun testAddItem() = runTest {
testDispatcher.scheduler.advanceUntilIdle()
val initialSize = viewModel.dataList.value.size
viewModel.addItem()
val newSize = viewModel.dataList.value.size
assertEquals("添加后应该有 ${initialSize + 1} 条数据", initialSize + 1, newSize)
assertTrue("最后一条数据应该是'新项目 11'",
viewModel.dataList.value.last().startsWith("新项目"))
}
// ============================================
// 测试3:删除数据
// ============================================
@Test
fun testDeleteItem() = runTest {
testDispatcher.scheduler.advanceUntilIdle()
val itemToDelete = "项目 1"
val initialSize = viewModel.dataList.value.size
viewModel.deleteItem(itemToDelete)
val newSize = viewModel.dataList.value.size
assertEquals("删除后应该有 ${initialSize - 1} 条数据", initialSize - 1, newSize)
assertFalse("'项目 1' 应该已被删除",
viewModel.dataList.value.contains(itemToDelete))
}
// ============================================
// 测试4:清空数据
// ============================================
@Test
fun testClearAll() = runTest {
testDispatcher.scheduler.advanceUntilIdle()
viewModel.clearAll()
assertTrue("清空后数据应该为空", viewModel.dataList.value.isEmpty())
}
// ============================================
// 测试5:切换模式
// ============================================
@Test
fun testToggleMode() = runTest {
testDispatcher.scheduler.advanceUntilIdle()
// 默认应该是列表模式(false)
assertFalse("初始应该是列表模式", viewModel.isGridMode.value)
viewModel.toggleMode()
assertTrue("切换后应该是网格模式", viewModel.isGridMode.value)
viewModel.toggleMode()
assertFalse("再次切换应该是列表模式", viewModel.isGridMode.value)
}
// ============================================
// 测试6:加载更多数据
// ============================================
@Test
fun testLoadMore() = runTest {
// ✅ 等待初始加载完成
testDispatcher.scheduler.advanceUntilIdle()
val initialSize = viewModel.dataList.value.size
assertEquals("初始应该有10条数据", 10, initialSize)
// 调用 loadMore
viewModel.loadMore()
// ✅ 等待所有协程完成(包括 delay(1000))
testDispatcher.scheduler.advanceUntilIdle()
val newSize = viewModel.dataList.value.size
assertEquals("加载更多后应该有 ${initialSize + 5} 条数据",
initialSize + 5, newSize)
// 验证新增的数据
val lastItems = viewModel.dataList.value.takeLast(5)
assertEquals("新增的第一条应该是'加载项目 11'",
"加载项目 11", lastItems[0])
assertEquals("新增的最后一条应该是'加载项目 15'",
"加载项目 15", lastItems[4])
}
// ============================================
// 测试7:刷新数据
// ============================================
@Test
fun testRefreshData() = runTest {
testDispatcher.scheduler.advanceUntilIdle()
// 先添加一些数据
viewModel.addItem()
viewModel.addItem()
testDispatcher.scheduler.advanceUntilIdle()
val beforeRefreshSize = viewModel.dataList.value.size
assertTrue("添加后数据应该多于10条", beforeRefreshSize > 10)
// 刷新数据
viewModel.refreshData()
testDispatcher.scheduler.advanceUntilIdle()
val afterRefreshSize = viewModel.dataList.value.size
assertEquals("刷新后应该恢复到10条数据", 10, afterRefreshSize)
assertEquals("刷新后第一条应该是'项目 1'", "项目 1", viewModel.dataList.value[0])
}
// ============================================
// 测试8:加载状态
// ============================================
@Test
fun testLoadingState() = runTest {
// 初始加载中
assertTrue("初始加载状态应该为 true", viewModel.initialLoading.value)
assertTrue("加载状态应该为 true", viewModel.loading.value)
// ✅ 等待加载完成
testDispatcher.scheduler.advanceUntilIdle()
assertFalse("加载完成后 initialLoading 应该为 false",
viewModel.initialLoading.value)
assertFalse("加载完成后 loading 应该为 false",
viewModel.loading.value)
// 测试加载更多时的状态
viewModel.loadMore()
//调用 viewModel.loadMore() 后,loading 状态在测试代码断言之前就已经被设置为 false 了
assertFalse("加载更多时 loading 应该为 false", viewModel.loading.value)
testDispatcher.scheduler.advanceUntilIdle()
assertFalse("加载更多完成后 loading 应该为 false",
viewModel.loading.value)
}
// ============================================
// 测试9:多次调用 loadMore 不会重复加载
// ============================================
@Test
fun testLoadMoreCalledMultipleTimes() = runTest {
testDispatcher.scheduler.advanceUntilIdle()
val initialSize = viewModel.dataList.value.size
// 第一次调用
viewModel.loadMore()
testDispatcher.scheduler.advanceUntilIdle()
val sizeAfterFirst = viewModel.dataList.value.size
assertEquals("第一次加载应该增加5条", initialSize + 5, sizeAfterFirst)
// 第二次调用(应该能正常加载)
viewModel.loadMore()
testDispatcher.scheduler.advanceUntilIdle()
val sizeAfterSecond = viewModel.dataList.value.size
assertEquals("第二次加载应该再增加5条", initialSize + 10, sizeAfterSecond)
}
// ============================================
// 测试10:没有数据时不能加载更多
// ============================================
@Test
fun testLoadMoreNotCalledWhenEmpty() = runTest {
testDispatcher.scheduler.advanceUntilIdle()
// 清空数据
viewModel.clearAll()
assertEquals("数据应该为空", 0, viewModel.dataList.value.size)
// 尝试加载更多
viewModel.loadMore()
testDispatcher.scheduler.advanceUntilIdle()
// 数据应该仍然为空
assertEquals("没有数据时不能加载更多", 0, viewModel.dataList.value.size)
}
// ============================================
// 测试11:验证数据内容
// ============================================
@Test
fun testDataContent() = runTest {
testDispatcher.scheduler.advanceUntilIdle()
val dataList = viewModel.dataList.value
assertEquals("第一条应该是'项目 1'", "项目 1", dataList[0])
assertEquals("第二条应该是'项目 2'", "项目 2", dataList[1])
assertEquals("最后一条应该是'项目 10'", "项目 10", dataList[9])
// 验证所有数据都是正确的格式
dataList.forEachIndexed { index, item ->
val expectedNumber = index + 1
assertEquals("第 ${index + 1} 条应该是 '项目 $expectedNumber'",
"项目 $expectedNumber", item)
}
}
}
项目模块的build.gradle
dependencies {
...
testImplementation libs.junit
testImplementation libs.kotlinx.coroutines.test // ✅ 添加协程测试
}
[versions]
agp = "9.1.0-alpha02"
...
# ✅ 添加协程版本
kotlinxCoroutines = "1.7.3"
[libraries]
...
# ✅ 添加协程测试依赖
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }