compose编码02- 实现类似AbsListView 测试用例

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" }

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容