SuperAPI AI 工具集

understanding-the-testing-pyramid

测试与质量

查看项目

更新于 10-16 02:02

技能介绍

用途

本技能用于为 Android 测试套件选择规模框架并保持一致,解决团队常见问题:每个行为只写一个大 instrumented 测试、跳过底层逻辑的单元覆盖,导致 CI 变慢、测试腐化。它提供一套统一的词汇,供后续技能(如选择测试对象、应用测试策略)复用。

两种框架,只能二选一

Google 在相邻页面给出两种框架,二者不矛盾,但不应混用。

框架 A — Small / Medium / Big(3 层,本技能推荐)

来自 /training/testing/fundamentals:单元测试或 small 测试只验证应用很小的一部分,如一个方法或类;端到端测试或 big 测试同时验证应用的较大部分,如整个屏幕或用户流程;medium 测试介于两者之间,检查两个或多个单元之间的集成。

  • Small:覆盖一个方法或类;典型运行器 JUnit4 + Mockito/MockK + fakes;典型执行 src/test/(JVM)。
  • Medium:2 个以上类集成,可能通过 Robolectric 使用 Android 框架;JUnit4 + Robolectric 或进程内 Android;src/test/(宿主)或 src/androidTest/(instrumented)。
  • Big:整个屏幕、用户流程或 release 构建冒烟;AndroidJUnitRunner + Espresso/Compose-test/UiAutomator;src/androidTest/。

框架 B — Unit / Component / Feature / Application / Release Candidate(5 层)

来自 /training/testing/fundamentals/strategies 的 scalable strategy 表。多数团队保留更简单、引用更多的框架 A;只有在设计多团队 CI 策略、额外粒度确有价值时才用框架 B。同一文档中不得混用两种框架。

作用域与执行位置相互独立

这是本页最重要的一点:并非所有单元测试都是本地的,也并非所有端到端测试都运行在设备上。例如 big 本地测试可使用本地运行的 Android 模拟器如 Robolectric;small instrumented 测试可验证代码与框架特性(如 SQLite 数据库)的协作。因此存在两个轴:作用域(small/medium/big)与执行位置(本地 JVM / instrumented 设备)。Robolectric 是本地 Android 模拟器,运行在 JVM 的 src/test/ 下,不是 instrumented 测试,其作用域可为 small、medium 或 big。

定性的金字塔

/strategies 页面只承诺定性形状:"大多数应用应该有 many small tests 和 relatively few big tests,形成金字塔形状。"以及关键点:一般应尽早添加测试,通常意味着从 small 测试开始。这就是 Google 当前全部量化主张:多小少大,没有比例。

70/20/10 比例的真正来源

"70% 单元、20% 集成、10% 端到端"被广泛引用,但并不存在于当前的 fundamentals 或 strategies 页面。该数字来自 Google 的工程文献 Software Engineering at Google 第 11 章(https://abseil.io/resources/swe-book),而非 Android 培训页面。不得将 70/20/10 归属于 developer.android.com。书中将其视为启发式而非定律,不同领域适用不同比例。

成本与速度权衡

  • Small:每测试 1-50 ms;失败信号可定位到类/方法;维护成本低。
  • Medium(Robolectric):50-500 ms;定位到组件;维护成本中。
  • Big(instrumented):5-30 s;只说"屏幕坏了";维护成本高(flake、模拟器状态、动画)。

扁平或倒置的金字塔(大量 big、少量 small)会导致:CI 慢(同等覆盖下比单元密集套件慢 10 到 100 倍);flake 率高,且 flake 与测试规模和外部依赖相关;失败信号模糊;重构痛苦,UI 测试对渲染文本断言,文案一变就断。/training/testing/instrumented-tests 明确指出:仅在你必须针对真实设备行为测试时才推荐使用 instrumented 测试。

何时有意打破金字塔

金字塔是默认值,不是定律。可在以下情形打破:产品本身就是集成(如主要串联三家 vendor SDK 的支付 SDK);框架拥有逻辑(纯 CRUD 屏幕、DataStore 支持的 ViewModel、原生 Material 组件,可单元测试内容很少);确定性是重点(渲染正确性套件、截图测试、动画时序);成本经济学翻转(Firebase Test Lab / Gradle Managed Devices 上 big 测试每分钟成本足够低,但维护成本仍占主导,且单元测试在 flake 上仍胜出)。

不构成打破理由的:"单元测试太难搭建"(这是架构不佳的信号);"我们无法 mock X"(应使用 fake 而非 mock);"在我机器上能跑,多上设备测试就行"(flake 会跟随套件)。

模式:错误 vs 正确

引用 70/20/10 时:错误做法是称 Google 开发者文档推荐 70/20/10(截至 2026-05-06 该比例不在 strategies 页面)。正确做法是说明 Android 文档只承诺定性金字塔,70/20/10 来自 Software Engineering at Google 第 11 章。

标注 Robolectric 时:错误做法是称其为 instrumented 测试并放入 src/androidTest/。正确做法是称其为本地(JVM)Android 模拟器,作用域可为 small/medium/big,放入 src/test/,仅需真实设备的测试才用 src/androidTest/。

单一设计文档中的框架:错误做法是混用框架 A(small/medium)与框架 B(feature/release candidate)。正确做法是选定框架 A 并坚持。

决策矩阵

  • 是否依赖 Android 框架?否 → Small(src/test/);通过 Robolectric 模拟 → Medium(src/test/);必须是真实设备 → Big(src/androidTest/)。
  • 是否涉及多个类?否 → Small;是但无 I/O 无 UI → Medium;是且含 UI 渲染或系统服务 → Big。
  • 是否跨越网络或进程边界?否 → Small 或 Medium;是(mock)→ Medium(框架 B 的 Feature);是(真实)→ Big(框架 B 的 Application/RC)。

强制规则

默认技能文案和设计文档必须使用 3 层 small/medium/big 框架;仅显式调用 /strategies 页面时才用 5 层框架。不得在同一文档混用两种框架。不得将 70/20/10 归属于 developer.android.com,应引用 Software Engineering at Google。不得将 Robolectric 描述为 instrumented 测试。必须通过注释或命名约定(androidx.test.filters 的 @SmallTest、@MediumTest、@LargeTest)为每个新测试标注作用域,以便 CI 分片。首选:按 Google 指导从 small 测试开始新模块,仅在 small 无法覆盖处添加 medium/big。首选:CI 慢时,先删除冗余 big 测试,再增加 small 测试——金字塔靠削顶而非仅拓宽底部来收紧。

验证

  • 每个测试类可通过 @SmallTest、@MediumTest、@LargeTest 之一或 source-set 位置到达。
  • src/androidTest/ 中没有可等价作为 Robolectric 宿主测试在 src/test/ 运行而不损失保真度的测试。
  • CI 日志显示每次变更各层单元测试墙钟时间小于 instrumented 测试墙钟时间。
  • 没有设计文档将 70/20/10 归属于 developer.android.com。
  • 没有设计文档混用框架 A 与框架 B 词汇。
  • 团队测试策略文档的术语表恰好命名一种框架。

不适用场景

  • 想知道每个测试放什么(哪些屏幕、哪些 ViewModel)→ 使用 ../choosing-what-to-test/SKILL.md。
  • 想配置 testImplementation 与 androidTestImplementation 的 Gradle 配置 → 使用 ../../strategies/organizing-test-source-sets/SKILL.md。
  • 想在 Fake / Mock / Stub / Spy 之间选择 → 使用 ../../doubles/picking-test-doubles/SKILL.md。
  • 调试单个 flaky 测试 → 从 ../../../compose/synchronization/synchronizing-with-idle/SKILL.md 或 ../../../jvm-tests/coroutines/testing-coroutines-with-runtest/SKILL.md 开始。

前置条件

一个至少含一个测试类的模块;熟悉 src/test/(本地 JVM)与 src/androidTest/(instrumented)的划分;理解 scope(small/medium/big)与执行位置(local/instrumented)是两个独立轴。

参考资料

developer.android.com/training/testing/fundamentals;developer.android.com/training/testing/fundamentals/strategies;developer.android.com/training/testing/instrumented-tests;Software Engineering at Google 第 11 章(https://abseil.io/resources/swe-book);tasks/research/R8-android-fundamentals.md;androidx.test.filters。