关于 understanding-the-testing-pyramid
面向 Android 测试设计者的技能指南,介绍 small、medium、big 三层测试范围,并与五层划分方式区分。它说明测试范围与本地或设备执行是不同维度,也澄清测试金字塔的定性建议及 70/20/10 比例的出处。
主要功能
- 01说明 small、medium、big 三层划分
- 02区分测试范围与执行位置
- 03对比三层与五层框架
- 04澄清测试金字塔的定性建议
- 05说明 70/20/10 的出处
适用场景
- 01为 Android 项目规划测试组合
- 02讨论本地测试与设备测试的分类
- 03评估测试套件过慢或测试范围混用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill understanding-the-testing-pyramid安装后在 Claude Code 中输入/understanding-the-testing-pyramid确认已加载。
详细介绍
理解测试金字塔——选择 3 层框架并坚持使用
当团队为每种行为都编写一个大型插桩测试,却不为底层逻辑编写单元测试时,Android 测试套件会拖慢 CI,并逐渐失去维护。Google 的 /training/testing/fundamentals 页面用三个范围——小型、中型、大型——来划分测试规模,/strategies 页面则在此基础上勾勒出一个定性金字塔。本技能采用 3 层框架,介绍另一种 5 层框架,避免作者将两者混为一谈,并如实说明广为流传的数字比例是从哪里来的。后续技能(../choosing-what-to-test/SKILL.md、../../strategies/applying-testing-strategies/SKILL.md)都以这套术语为基础。
何时使用本技能
- 用户询问“我应该编写多少个单元测试和 UI 测试”,或者引用 70/20/10 / 80/15/5 的比例并询问 Google 文档中是否有这样的说法。
- 用户把所有测试都称为“集成测试”或“端到端测试”,而 Agent 需要一套术语来对测试进行分类。
- 用户反映“CI 很慢”或“测试套件要跑 40 分钟”,并想知道是否应该删除插桩测试。
- 用户询问 Robolectric 算不算“单元测试”或“集成测试”——在 Google 的框架中,它属于本地中型测试。
- 用户正在启动一个新模块,并询问从哪里开始(回答:根据
/strategies,先编写小型测试)。
何时不使用本技能
- 用户想知道每种测试应该覆盖什么(哪些屏幕、哪些 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。 - 用户正在调试单个不稳定测试并想修复它——从
../../../compose/synchronization/synchronizing-with-idle/SKILL.md或../../../jvm-tests/coroutines/testing-coroutines-with-runtest/SKILL.md开始。
先决条件
- 一个至少有一个测试类的模块,以便讨论更具体。
- 熟悉
src/test/(本地 JVM)与src/androidTest/(插桩测试)的划分。如果不熟悉,请先阅读../../strategies/organizing-test-source-sets/SKILL.md。 - 理解“范围”(小型/中型/大型)与“执行位置”(本地/插桩)是 Google 定义的两个独立维度。
两种框架(每项技能 / 每份设计文档中只能选一种)
Google 在相邻页面上发布了两种框架。两者并不矛盾,只是看问题的角度不同。
框架 A——小型 / 中型 / 大型(3 层)← 本技能推荐使用此框架
引自 /training/testing/fundamentals(“Android 中的测试类型” → “按范围划分”):
“测试也会按照范围进行分类,即测试覆盖的代码量。测试范围分为三类:
- 单元测试或小型测试仅验证应用中一小部分代码,例如一个方法或类。
- 端到端测试或大型测试同时验证应用的较大部分,例如整个屏幕或用户流程。
- 中型测试介于两者之间,用于检查两个或更多单元之间的集成。” —
developer.android.com/training/testing/fundamentals
| 范围 | 覆盖内容 | 典型运行器 | 典型执行位置 |
|---|---|---|---|
| 小型 | 一个方法或类 | JUnit4 + Mockito/MockK + fake | src/test/(JVM) |
| 中型 | 集成的 2 个或更多类,可能通过 Robolectric 使用 Android 框架 | JUnit4 + Robolectric 或进程内 Android | src/test/(宿主机)或 src/androidTest/(插桩测试) |
| 大型 | 整个屏幕、用户流程或发布构建冒烟测试 | AndroidJUnitRunner + Espresso/Compose-test/UiAutomator | src/androidTest/ |
框架 B — 单元 / 组件 / 功能 / 应用 / 发布候选版本(5 层)
来自 /training/testing/fundamentals/strategies(“可扩展策略”表):
| 级别 | 范围 | 网络 |
|---|---|---|
| 单元 | 依赖最少的单个方法或类 | 无 |
| 组件 | 模块或组件 | 无 |
| 功能 | 多个组件 / 模块 | “支持模拟网络访问” |
| 应用 | 整个应用,可调试的二进制文件 | 不适用 |
| 发布候选版本 | 整个发布构建,已压缩 | 不适用 |
大多数团队采用 3 层的框架 A,因为这种形式更简单、引用也更多。只有在设计多团队 CI 策略,且更精细的划分确实有用时,才采用框架 B。绝不能在同一份文档中混用二者——选定一种并贯彻到底。
范围与执行位置是正交的
这是页面上最重要的一段引文,也是开发者说“我的所有测试都是插桩测试”时 Agent 应引用的内容:
“并非所有单元测试都在本地运行,也并非所有端到端测试都在设备上运行。例如:
- 大型本地测试:你可以使用在本地运行的 Android 模拟器,例如 Robolectric。
- 小型插桩测试:你可以验证代码是否能与某项框架功能(例如 SQLite 数据库)良好协作。” —
developer.android.com/training/testing/fundamentals
两个维度:
LOCAL (JVM) INSTRUMENTED (device)
SMALL pure JUnit + fakes unit test of SQLite via real DB
MEDIUM Robolectric component component test on real Android
BIG Robolectric flow Espresso / Compose UI flow
Robolectric 是一个本地 Android 模拟器。它不是“插桩测试”——它在 src/test/ 下通过 JVM 运行。请参阅 ../../strategies/organizing-test-source-sets/SKILL.md 和跨类别技能 ../../../jvm-tests/robolectric/using-robolectric-correctly/SKILL.md。
定性金字塔
/strategies 页面只确定了定性的形态:
“大多数应用都应包含许多小型测试和相对较少的大型测试,形成金字塔形。” —
developer.android.com/training/testing/fundamentals/strategies
“**要点:**一般来说,应尽量在开发周期的早期添加测试。这通常意味着从小型测试开始。” —
developer.android.com/training/testing/fundamentals/strategies
这就是 Google 当前关于数量的全部说法:“小型测试多,大型测试少”。没有给出比例。
70/20/10 比例的真正出处
“70% 单元测试、20% 集成测试、10% 端到端测试”这一规则广为引用,但在当前的 /training/testing/fundamentals 或 /training/testing/fundamentals/strategies 页面上并不存在(截至 2026-05-06;已在 tasks/research/R8-android-fundamentals.md 中核实)。这个数字来自 Google 自己的工程文献——Software Engineering at Google 第 11 章——而非 Android 培训页面。
绝不能将“70/20/10”归因于 developer.android.com。这个数字比例是 Software Engineering at Google 中广为流传的社区经验法则(https://abseil.io/resources/swe-book,第 11 章)——转述时务必谨慎,除非逐字核对过原文,否则不要将任何具体句子作为直接引文呈现。这本书将这种划分视为经验法则,而非定律:不同领域适用的比例各不相同;请参阅下文“何时打破金字塔”。
成本与速度的权衡
金字塔底部较宽的原因:
| 层级 | 每项测试的实际耗时 | 失败信号 | 维护成本 |
|---|---|---|---|
| 小型 | 1-50 ms | 精确定位到某个类或方法 | 低 |
| 中型(Robolectric) | 50-500 ms | 精确定位到某个组件 | 中 |
| 大型(插桩测试) | 5-30 s | 只会说“屏幕坏了” | 高(不稳定、模拟器状态、动画) |
扁平或倒置的金字塔(大量大型测试、少量小型测试)会导致:
- CI 变慢(比覆盖率相当、以单元测试为主的测试套件慢 10 到 100 倍)。
- 不稳定率高;不稳定性与测试规模和外部依赖相关(
/strategies指出,单元测试层和组件测试层“无网络访问”)。 - 失败信号模糊——“X 屏幕坏了”,而不是“函数 Y 对输入 Z 返回了错误值”。
- 重构痛苦——UI 测试会断言渲染出的文本,文案每次变更都会导致测试失败。
/training/testing/instrumented-tests 明确说明了代价:
“我们建议仅在必须针对真实设备的行为进行测试时使用插桩测试。” —
developer.android.com/training/testing/instrumented-tests
这就是 Google 用自己的话表达的“将大型测试作为最后手段”。
有意打破金字塔的情况
金字塔是默认做法,而不是铁律。以下情况可以打破它:
- 产品本身就是集成。 一个主要负责连接三个供应商 SDK 的支付 SDK,在功能或应用测试中获得的价值高于对胶水代码进行单元测试。单元测试只会对模拟对象进行同义反复式的验证。
- 逻辑由框架负责。 一个纯 CRUD 屏幕,使用由 DataStore 支持的
ViewModel和标准 Material 组件,几乎没有什么需要进行单元测试。大型测试可以发现连接问题;小型测试则只是在测试runBlocking { dataStore.data.first() }。 - 确定性是重点。 渲染正确性测试套件(截图测试、动画计时)属于大型测试层,因为缺陷会在那里显现。参见跨类别技能
../../../compose/audit/auditing-compose-test-suite/SKILL.md。 - 成本效益发生逆转。 在 Firebase Test Lab / Gradle Managed Devices 上,大型测试的每分钟成本可能低到维护成本反而占主导——而单元测试在稳定性上仍然更胜一筹。
以下理由不足以证明打破金字塔是合理的:
- “单元测试太难搭建了”——这说明架构不佳,而不是堆积大型测试的理由。参见
developer.android.com/training/testing/fundamentals(“采用可测试的应用架构……”)。 - “我们无法模拟 X”——用伪实现代替模拟对象。参见
../../doubles/picking-test-doubles/SKILL.md。 - “它在我的机器上能运行,多加些设备测试直接发布吧”——不稳定性会蔓延到整个测试套件。使用
../../../adb/observability/extracting-logs-with-logcat/SKILL.md进行诊断。
模式
模式:错误与正确——引用 70/20/10 比例
> Per Google's developer documentation, the testing pyramid recommends
> a 70/20/10 split between unit, integration, and end-to-end tests.
> Source: developer.android.com/training/testing/fundamentals/strategies
错误原因:截至 2026-05-06,策略页面上并没有这个比例。页面只定性地说“多做小型测试,少做大型测试”。此处引用 developer.android.com 属于捏造。
> Google's Android docs commit to a qualitative pyramid only:
> "Most apps should have many small tests and relatively few big tests"
> (developer.android.com/training/testing/fundamentals/strategies).
> The familiar 70/20/10 split comes from Software Engineering at Google,
> ch. 11 (https://abseil.io/resources/swe-book), not the Android docs.
模式:错误与正确——对 Robolectric 进行分类
> Robolectric tests are instrumented tests because they exercise the
> Android framework. Put them in src/androidTest/.
错误原因:Robolectric 在 JVM 上运行,不需要 Android 设备或模拟器。根据 /training/testing/fundamentals(“大型本地测试:可以使用本地运行的 Android 模拟器,例如 Robolectric”),无论范围如何,Robolectric 都属于 src/test/ 中的本地测试。
> Robolectric is a local (JVM) Android simulator. By scope it can be small,
> medium, or big depending on what the test exercises. Place it in src/test/.
> Use src/androidTest/ only for tests that require a real device.
模式:错误与正确——在一份设计文档中使用金字塔表述
> Our suite has 200 small tests, 40 medium tests, 8 feature tests, and
> 3 release-candidate tests, which is a healthy pyramid.
错误原因:这混淆了表述方式 A(“小型/中型”)和表述方式 B(“功能/候选发布版”)。读者无法判断这里的“小型”是指表述方式 A 中的小型,还是表述方式 B 中的单元测试。
> Our suite has 200 small tests, 40 medium (Robolectric component) tests,
> and 11 big (instrumented Compose-test + Espresso) tests. Per Google's
> small/medium/big framing on /training/testing/fundamentals, the ratio
> is roughly 80/16/4, in line with the pyramid recommendation.
决策矩阵——新测试应该采用什么范围?
Question → Scope
----------------------------------------------------------
"Does it depend on the Android framework?"
No → Small (src/test/)
Yes, simulated via Robolectric → Medium (src/test/)
Yes, must be a real device → Big (src/androidTest/)
"Does it exercise more than one class?"
No → Small
Yes, but no I/O / no UI → Medium
Yes, with UI rendering or system services → Big
"Does it cross a network or process boundary?"
No → Small or Medium
Yes (mocked) → Medium (Feature in Framing B)
Yes (real) → Big (Application/RC in Framing B)
强制规则
- 默认技能文案和设计文档必须使用“小 / 中 / 大”三层框架。只有在明确使用
/strategies页面时,才采用“Unit / Component / Feature / Application / Release Candidate”五层框架。 - 不得在同一份文档中混用这两种框架。择一使用。
- 不得将“70/20/10”这一数值比例归因于
developer.android.com。请引用 Software Engineering at Google(https://abseil.io/resources/swe-book)。 - 不得将 Robolectric 描述为“instrumented test”。它是一种本地 Android 模拟器,可以承载任何范围的测试。
- 每个新测试必须通过注释或命名约定标注其范围(使用
androidx.test.filters中的@SmallTest、@MediumTest、@LargeTest),以便 CI 进行分片。 - **建议:**按照 Google 的“从小型测试开始”指南,新模块优先从小型测试开始;只有小型测试无法覆盖时,才添加中型 / 大型测试。
- **建议:**CI 运行缓慢时,先删除冗余的大型测试,再添加更多小型测试——通过削减顶部来收紧测试金字塔,而不只是拓宽底部。
验证
- 模块中的每个测试类都可通过
@SmallTest、@MediumTest、@LargeTest之一(或通过源集位置)访问。 -
src/androidTest/中没有测试可以在不降低保真度的情况下,改为在src/test/中作为 Robolectric 主机测试运行。(如果可以,请移动它。) - CI 日志报告的每次变更中各层的单元测试墙钟时间都小于 instrumented 测试墙钟时间。
- 没有设计文档将“70/20/10”归因于
developer.android.com。 - 没有设计文档混用框架 A 和框架 B 的术语。
- 团队的测试策略文档在术语表中只定义一种框架。
参考资料
developer.android.com/training/testing/fundamentals— 小 / 中 / 大范围、“并非所有单元测试都在本地运行”引文、可测试架构的理由。developer.android.com/training/testing/fundamentals/strategies— 定性金字塔(“大量小型测试,少量大型测试”)、五级 Unit / Component / Feature / Application / Release Candidate 表格、“从小型测试开始”指南。developer.android.com/training/testing/instrumented-tests— “仅在必须针对真实设备的行为进行测试时使用 instrumented tests”。- Software Engineering at Google,第 11 章 — 70/20/10 比例的出处。https://abseil.io/resources/swe-book
tasks/research/R8-android-fundamentals.md— 逐字引文、版本敏感说明、“页面上没有 70/20/10”的发现。androidx.test.filters— 用于运行时分片的@SmallTest/@MediumTest/@LargeTest注解。- 同级技能:
../choosing-what-to-test/SKILL.md、../../doubles/picking-test-doubles/SKILL.md、../../strategies/applying-testing-strategies/SKILL.md、../../strategies/organizing-test-source-sets/SKILL.md。 - 跨类别:
../../../jvm-tests/robolectric/using-robolectric-correctly/SKILL.md、../../../jvm-tests/runner/configuring-junit4-on-android/SKILL.md、../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md、../../../compose/audit/auditing-compose-test-suite/SKILL.md。