Plaid测试金字塔:单元测试、Robolectric集成测试与Espresso实战详解
【免费下载链接】plaidAn Android app which provides design news & inspiration as well as being an example of implementing material design.项目地址: https://gitcode.com/gh_mirrors/pl/plaid
Plaid 是一款开源的 Android 设计资讯应用,聚合 DesignerNews、Dribbble 与 ProductHunt 的设计新闻,同时是 Google 官方的 Material Design 实战范例。本文带你拆解 Plaid 的测试金字塔:如何用JUnit 单元测试测试 ViewModel、如何搭建集成测试(含 Robolectric 思路对比)、以及如何用Espresso做 UI 自动化测试。新手跟着看,就能在自己项目里复刻一套完整的 Android 测试体系。
📐 一、先看全局:Plaid 的测试金字塔长什么样?
测试金字塔的核心思想:单元测试最多最快,UI 测试最少最慢。Plaid 作为 MVVM 架构(ViewModel + LiveData + 协程 + Room + Dagger)的典范,把三层测试清晰映射到了 Gradle 的目录约定中:
| 测试层 | 源码目录 | 核心工具 | 运行位置 |
|---|---|---|---|
| 单元测试 | src/test/ | JUnit 4、Mockito、kotlinx-coroutines-test | JVM,秒级反馈 |
| 集成测试 | src/androidTest/ | AndroidJUnit4、Room 内存数据库 | 真机 / 模拟器 |
| UI 测试 | src/androidTest/ | Espresso、UiAutomator | 真机 / 模拟器 |
全部测试依赖集中在根目录的 test_dependencies.gradle 中统一管理,版本一览:
| 依赖 | 版本 | 用途 |
|---|---|---|
| junit | 4.12 | 测试框架 |
| mockito / mockito-kotlin | 2.23.0 / 2.0.0-RC3 | 依赖打桩 |
| espresso-core / espresso-contrib | 3.1.0-beta02 | UI 自动化 |
| androidx.test (runner / rules / ext-junit) | 1.1.0 / 1.1.0-beta02 | 仪器化测试基础设施 |
| kotlinx-coroutines-test | 1.3.0 | 协程调度控制 |
💡 值得学习的工程细节:每个功能模块(
app、core、designernews、dribbble、search、about)都各自带test与androidTest目录,测试随代码走,而不是堆在某个集中目录。
⚡ 二、单元测试:Mockito + 协程 Rule 测试 ViewModel
单元测试是金字塔的底座。以 HomeViewModelTest.kt 为例,Plaid 有一套非常可复用的“三件套”:
// 1) 替换主线程协程调度器,让协程在测试中同步可控 @get:Rule var coroutinesRule = MainCoroutineRule() // 2) 让 Architecture Components 的任务在当前线程执行 @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() // 3) 用 mock() 打桩所有外部依赖 private val dataManager: DataManager = mock() private val loginRepository: LoginRepository = mock()对应源码见 HomeViewModelTest.kt#L66-L76。测试用例本身遵循Given-When-Then注释范式,例如验证退出登录逻辑(HomeViewModelTest.kt#L92-L101):
- Given:构造一个 ViewModel
- When:调用
logoutFromDesignerNews() - Then:
verify(loginRepository).logout()验证仓库被正确调用
配套技巧:MockitoKotlinHelpers.kt 里封装了capture()与argumentCaptor<T>()两个 Kotlin 扩展,规避了 Mockito 泛型 API 在 Kotlin 中的繁琐写法;而capture的返回值被处理为可空类型,避免 Mockito 返回 null 时抛IllegalStateException——这种“为 Kotlin 修 Java API 毛刺”的写法很值得借鉴。
新手要点清单✅
- 被测类只 mock 依赖,不 mock 被测对象
- 用
whenever(...).thenReturn(...)打桩输入,用verify(...)断言输出 - 每个测试只验证一件事,方法名写成行为描述(如
isDesignerNewsLoggedIn)
🧪 三、集成测试:Plaid 的真机方案与 Robolectric 思路
提到 Android 集成测试,很多人第一反应是Robolectric——在 JVM 上模拟 Android API,无需设备即可运行。Plaid 的选择略有不同:它的集成测试放在src/androidTest/,以仪器化测试形式在真机/模拟器上运行,用AndroidJUnit4跑数据库、SharedPreferences 等真实组件。
典型代表是 Room 数据库测试 LoggedInUserDaoTest.kt,套路非常标准:
// @Before:建内存数据库(不落盘,每个用例干净隔离) database = Room.inMemoryDatabaseBuilder(context, DesignerNewsDatabase::class.java).build() // @After:关闭数据库 database.close()它验证 DAO 的setLoggedInUser/getLoggedInUser读写是否一致。由于 Repository 层基于协程,测试中用共享工具getOrAwaitValue()把挂起结果“await”出来再断言。
两种路线的取舍,一张表说清:
| 对比项 | Robolectric(JVM 集成) | Plaid 方案(真机仪器化) |
|---|---|---|
| 运行速度 | 快,无需设备 | 较慢,需模拟器/真机 |
| 保真度 | 模拟 Android API,存在偏差 | 真实运行时,保真度最高 |
| 适用场景 | 快速回归、CI 密集执行 | 数据库、文件、系统 API 等强依赖真实环境的逻辑 |
结论:如果你的集成点像 Plaid 一样集中在 Room 与本地存储,真机仪器化测试是最稳的写法;若希望 CI 提速,可以参考同样的测试代码引入 Robolectric,把@RunWith(AndroidJUnit4::class)换成 Robolectric 运行器,测试主体几乎不用改动。
📱 四、Espresso 实战:三步写一个 UI 测试
UI 测试是金字塔塔尖,Plaid 只写了 3 个,个个精炼。HomeActivityTest.kt 覆盖了主页的三种关键状态:
- 初始状态:
drawerClosedOnStartup—— 应用启动时抽屉应为关闭(L42-L50) - 交互行为:
pressFilterButtonOpensDrawer—— 点击过滤按钮后抽屉打开(L52-L62) - 配置变更:
drawerStaysOpenAfterRotation—— 旋转屏幕后抽屉应保持打开(L64-L75)
结构上就三个要素:
@Rule ActivityTestRule负责拉起被测 ActivityonView(withId(...)).perform(click())驱动点击check(matches(isOpen(isClosed)))做状态断言
几个细节值得抄作业:抽屉开合断言来自espresso-contrib的DrawerMatchers(这正是 test_dependencies.gradle 中同时引入 core 与 contrib 的原因);而屏幕旋转这种 Espresso 覆盖不到的设备级操作,Plaid 用UiAutomator补位:UiDevice.getInstance(...).setOrientationLeft()(见 HomeActivityTest.kt#L71)。Espresso 管 View 交互,UiAutomator 管设备级操作,分工明确。
🧰 五、跨层共享的测试资产
Plaid 用一个独立的 test_shared 模块沉淀跨层复用的工具,各模块通过testImplementation project(':test_shared')引入(见 test_dependencies.gradle#L17)。其中被反复引用的核心成员:
MainCoroutineRule:统一的主线程协程调度器替换规则getOrAwaitValue():等待并取出挂起结果的通用断言助手provideFakeCoroutinesDispatcherProvider()/runBlocking:配合项目自定义的 CoroutinesDispatcherProvider 注入假调度器
每个功能模块还各自维护一份 TestData.kt,提供story、shot、post、designerNewsSource等测试数据工厂——测试数据不依赖网络,全部本地构造,这是集成测试稳定不抖动的关键。
🚀 六、怎么跑起来
- 单元测试:执行
./gradlew test,全部在 JVM 上秒级反馈 - 集成测试 + Espresso:连接设备后执行
./gradlew connectedAndroidTest - 仓库的 scripts/ftl_run_tests.sh 提供了 CI 场景下的完整测试执行脚本,可参考其组织方式
结语:Plaid 的测试体系没有炫技,却把“该测什么、放哪层、用什么工具”示范得清清楚楚——ViewModel 交给 Mockito 快速打桩,数据库交给内存库真机验证,UI 只挑最高价值的三条路径交给 Espresso。照着这套金字塔组织自己的测试,你就拥有了可维护、可回归的 Android 测试基线。
【免费下载链接】plaidAn Android app which provides design news & inspiration as well as being an example of implementing material design.项目地址: https://gitcode.com/gh_mirrors/pl/plaid
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考