先看结论与判断条件
- 先固定最终 AAB、应用签名责任、模块清单和交付模式,再为目标设备生成或取得实际 APK 集,避免测试对象与生产交付不一致。
- 安装时、条件交付和按需交付是不同生命周期;base 首次启动通过不能代表尚未下载的 feature 能安装、加载和执行。
- 按需回归至少覆盖未安装查询、请求、下载、安装、取消、可恢复失败、重试、入口执行和卸载后再请求,而不只等待一次成功状态。
- bundletool 本地产物适合重现设备选择与拆分结构,但本地签名和交付路径必须显式记录,不能替代 Play 测试轨道的真实回执。
- 升级场景要同时确认已安装模块、未安装模块、旧进程状态和新候选入口,结论绑定 base 与派生 split 的同一版本身份。
- 本文只定义加固后的动态交付回归,不据此决定哪些模块或方法进入 VMP,也不虚构兼容率、性能或攻击阻断结果。
动态功能回归的对象不是单个 base APK
动态功能应用的发布输入是 AAB,设备最终运行的是根据设备配置和交付条件选择的 base、配置拆分以及 feature 拆分。Play Feature Delivery 允许模块在安装时、满足条件时或用户触发后交付,因此初次安装成功只证明当前集合可用。一个尚未交付的模块没有代码和资源可供 base 直接使用,加固回归必须把交付前、交付中和交付后的集合变化纳入同一测试计划。
加固会改变最终代码或打包产物,风险可能出现在多个边界:base 到 feature 的入口名称、资源引用、反射或组件发现、feature 内的 Native 库、拆分安装后的类加载、签名与版本一致性,以及失败后的状态清理。不能看到 base 首页正常就推断 feature 也正常,也不能只验证模块显示为 installed 而不进入真实业务页面、执行关键输入并观察退出与重进。
范围还要和保护策略分开。本文回答如何验证动态功能在加固候选中的下载、安装、加载、升级和回退,不回答哪些 feature 或方法应进入 VMP。保护清单应由另一个资产评审产生;这里把它当作候选身份的一部分,用于解释差异。这样测试失败可以定位到交付、签名、加载、资源、保护或业务入口,而不是把所有现象归到动态模块。
| 对象 | 需要固定 | 主要回归 | 不能直接推断 |
|---|---|---|---|
| 最终 AAB | 摘要、版本、模块与构建证明 | 结构与派生输入 | 设备收到相同文件集合 |
| base APK | 包名、版本、证书与摘要 | 启动、模块请求和公共入口 | 未交付 feature 已可用 |
| 配置 split | ABI、密度、语言条件 | 目标设备选择和资源加载 | 其他设备选择一致 |
| feature split | 模块名、交付模式和摘要 | 下载、安装、入口与升级 | 显示 installed 即业务通过 |
| 安装会话 | sessionId、状态、错误与时间线 | 取消、失败、重试与恢复 | 一个会话代表所有网络 |
| 设备现场 | 设备规格、安装集合与账号 | 真实加载和业务路径 | 本地模拟等同商店交付 |
先枚举模块、交付模式和跨模块契约
测试清单从构建配置和 AAB 模块视图生成,至少记录模块稳定名称、是否按需、是否安装时交付、条件表达式、依赖的 base API、公开组件、深链入口、资源命名、Native ABI、预计触发页面和失败替代路径。Play Feature Delivery 的交付模式决定模块何时可能存在,测试用例不能把条件模块和按需模块都当成首次安装必有文件。
跨模块契约比页面名称更重要。base 可能通过显式 Activity、导航路由、接口实现、反射类名、资源 ID、ServiceLoader 或 JNI 入口到达 feature。加固与优化可能改变名称或可达性,资源处理也可能改变查找结果。每条契约需要唯一标识、所有者、调用方向、预期失败行为和测试入口;仅保存模块包名不足以解释为什么下载成功后仍无法打开。
条件交付需要固定条件证据。设备 API、硬件能力、地区或其他配置可能影响是否交付,测试报告必须记录实际 device spec 与模块选择结果。未满足条件时没有模块不等于失败;满足条件却缺少预期模块才是差异。按需模块则从未安装状态开始,确认查询结果、发起请求、状态监听和入口门控,避免用户在模块到达前直接触发不存在的类或资源。
| 字段 | 回答的问题 | 验证来源 | 缺失时动作 |
|---|---|---|---|
| moduleName | 请求和状态使用哪个稳定名称 | AAB 模块与构建配置 | 阻断自动测试 |
| deliveryMode | 安装时、条件还是按需 | Feature Delivery 配置 | 不得猜测首次集合 |
| entryContract | 安装后怎样进入功能 | Manifest、导航和接口 | 补真实业务入口 |
| deviceCondition | 哪些设备应收到模块 | 目标 device spec | 标记未判定 |
| nativeAbis | 模块是否携带 Native 库 | 模块文件与 ABI 清单 | 扩大设备矩阵 |
| fallback | 下载失败时用户看到什么 | 产品契约和测试断言 | 不能以崩溃为回退 |
用同一 AAB 派生可重现的设备 APK 集
Build and test Android App Bundles 说明可以使用 bundletool 从 AAB 生成 APK set,并按连接设备或 device spec 安装适配集合。回归时先保存最终 AAB 摘要,再保存 bundletool 版本、构建命令参数、device spec 摘要、生成的 apks 文件摘要以及选中的 APK 列表。相同 AAB 面向不同 ABI、密度、语言和系统版本可能产生不同集合,不能用一台设备的 split 列表当成全局基线。
bundletool 适合检查和重现派生结构,但签名必须明确。bundletool 文档说明生成 APK 时可以提供签名信息;未显式提供发布签名材料的本地测试产物不能被标成生产候选。若生产采用 Play App Signing,本地 set 与 Play 实际交付可能经过不同签名责任和分发路径,因此报告要把“本地结构回归”“本地设备安装”和“测试轨道交付”分成不同证据等级。
设备现场还要记录当前已安装集合。测试开始前确认包版本、base、配置 split、已安装 feature、证书摘要和残留会话;必要时分别执行全新安装与保留数据升级。只删除应用数据不会移除安装包集合,只覆盖安装也可能保留已安装模块。若前置状态不固定,某次按需请求立即成功可能只是模块已经存在,而不是下载、安装和加载流程本次真正通过。
| 证据层 | 能验证 | 必须记录 | 主要限制 |
|---|---|---|---|
| 本地 Fake 管理器 | 状态机、错误和界面分支 | 场景、注入条件与断言 | 不经过真实商店服务 |
| bundletool 设备安装 | 设备选择、拆分结构和本地加载 | AAB、device spec、签名与 set | 不等于 Play 在线交付 |
| Play 测试轨道 | 真实账号与服务交付路径 | 轨道、版本、账号、设备和会话 | 覆盖仍受设备与条件限制 |
| 静态 AAB 检查 | 模块声明和依赖结构 | 工具版本与摘要 | 不证明运行入口正常 |
| 设备 instrumented test | 指定候选的运行语义 | 设备、输入、模块集合和结果 | 单一设备不能外推矩阵 |
| 生产监控 | 真实故障与采用分布 | 版本、状态和隐私边界 | 不能替代发布前拒绝路径 |
把按需安装当成可恢复状态机测试
按需模块不是一个同步布尔值。请求可能经历 pending、downloading、downloaded、installing、installed,也可能等待用户确认、被取消或以错误结束。具体状态与错误以使用的 Play Feature Delivery API 为准。测试应保存 sessionId、模块集合、每次状态、错误码、已下载字节、总字节、时间戳和触发动作;业务界面只在确认模块可用后进入,不用最后一次回调覆盖完整时间线。
取消场景至少覆盖请求后立即取消和下载过程中取消,观察最终状态、临时 UI、再次请求以及进程重建后的状态。取消不是删除已经安装的模块,也不应被实现成全局失败。若 API 在特定阶段无法接受取消,产品要呈现明确的继续或等待状态。回归结论记录实际观察,不凭测试脚本预期伪造平台结果。
失败与重试要区分可恢复和不可恢复原因。网络中断、空间不足、服务不可用、会话冲突、模块名错误或版本不匹配需要不同动作。测试不能把所有错误都立即无限重试;应验证退避、用户提示、重复点击的幂等、旧 session 清理和 base 功能仍可用。On-demand feature delivery testing 提供本地测试手段,但故障注入通过只说明业务分支可执行,真实轨道仍需复验。
| 场景 | 起始条件 | 关键断言 | 后续动作 |
|---|---|---|---|
| 首次请求 | 模块未安装 | 产生会话并显示可解释进度 | 等待安装后进入 |
| 重复请求 | 已有活动会话 | 不创建冲突业务流程 | 复用或明确拒绝 |
| 主动取消 | 下载或等待阶段 | 状态和 UI 最终一致 | 允许按规则重试 |
| 网络失败 | 故障注入或真实断网 | 错误可见且 base 继续可用 | 恢复网络后新会话 |
| 安装完成 | 状态为 installed | 模块集合更新且入口可执行 | 重启后再次验证 |
| 进程恢复 | 会话中途杀进程 | 重新查询而非沿用内存布尔值 | 恢复正确页面状态 |
安装成功后还要验证代码、资源和业务入口
installed 只表示交付管理器认为模块可用,不能代表 feature 内所有契约都通过。下一步从真实入口打开 Activity、Fragment、Compose 路由或其他公开能力,验证类加载、资源、主题、字符串、导航参数、持久状态和异常边界。若入口由字符串、反射或框架发现,核对优化与保留规则;若使用模块资源,验证不同语言、密度与夜间模式,不只看默认配置。
feature 携带 Native 库时还要覆盖目标 ABI、库加载顺序、JNI 注册和依赖解析。模块未安装时 base 不应提前加载其中的库;安装完成后应在目标线程和生命周期调用真实入口。一个 ABI 成功不能代表其他 ABI,模拟器通过也不能替代所需真机。本文不提供通用兼容结论,矩阵应根据实际模块携带的库和最低系统版本建立。
业务验证必须包含拒绝与回退路径。模块安装后若账号无权限、服务端配置关闭或依赖数据缺失,应回到可理解的业务状态,而不是把交付成功当成授权成功。模块安装失败时,base 的核心导航和非相关能力仍应可用。相关设备选择方法可参照同站的 [加固验收设备、API 与 ABI 矩阵](/zh-cn/articles/device-api-abi-hardening-test-matrix/),但当前候选仍需独立列出实际覆盖。
- 从真实导航或组件入口进入模块
- 验证类加载、参数、资源、主题与配置变化
- Native 模块按实际 ABI 覆盖加载和 JNI
- 安装后杀进程并重新进入功能
- 无权限与配置关闭不被交付状态绕过
- 模块失败不破坏 base 的非相关能力
用统一驱动执行安装、取消、失败和升级场景
自动化需要把业务场景与具体交付环境分开。定义一个 FeatureDeliveryDriver,由本地 Fake 管理器、bundletool 安装环境和测试轨道设备各自提供适配器;场景层只关心模块集合、会话状态、入口结果和候选版本。这样同一组断言可以在不同证据层执行,同时清楚标记哪个适配器能够注入网络失败,哪个适配器代表真实服务。
下面 Kotlin 示例是公开安全的测试编排核心,不包含证书、账号、内部模块名或攻击操作。它枚举允许模块,执行首次安装、取消、网络失败后重试、安装后入口、进程恢复以及 base 升级。驱动必须把每一步落到对应测试环境的官方 API,并返回真实状态;示例故意不把 Fake 结果伪装成轨道回执,也不设置任何性能阈值。
生产项目应为驱动增加证据写入:候选摘要、适配器类型、设备规格、开始集合、sessionId、状态序列、错误和最终集合。resetToBase 必须是受控测试操作,只能在测试设备清理到指定基线;upgradeTo 接受已生成且已签名的目标候选,不能在测试中临时重签。断言失败时保留现场,不继续运行依赖前一步的场景,以免第二个错误覆盖第一处差异。
- 每个模块从受控未安装基线开始
- 适配器明确标记 Fake、本地或测试轨道
- 每个 session 保存完整状态序列和错误
- 取消和失败后确认模块集合没有误更新
- 重试、进程恢复和升级使用真实入口断言
- 失败立即保留现场并停止依赖场景
enum class DeliveryState { REQUESTED, DOWNLOADING, INSTALLED, CANCELED, FAILED }
data class FeatureCase(
val moduleName: String,
val entryId: String
)
interface FeatureDeliveryDriver {
fun resetToBase(candidateId: String)
fun installedModules(): Set<String>
fun request(moduleName: String): Int
fun awaitTerminal(sessionId: Int): DeliveryState
fun cancel(sessionId: Int): DeliveryState
fun injectNetworkFailure(enabled: Boolean)
fun launchEntry(entryId: String): Boolean
fun recreateProcess()
fun upgradeTo(candidateId: String)
}
fun runDeliveryMatrix(
driver: FeatureDeliveryDriver,
baseCandidate: String,
upgradeCandidate: String,
cases: List<FeatureCase>
) {
require(cases.isNotEmpty())
require(cases.map { it.moduleName }.toSet().size == cases.size)
for (case in cases) {
driver.resetToBase(baseCandidate)
check(case.moduleName !in driver.installedModules())
val installSession = driver.request(case.moduleName)
check(driver.awaitTerminal(installSession) == DeliveryState.INSTALLED)
check(case.moduleName in driver.installedModules())
check(driver.launchEntry(case.entryId))
driver.resetToBase(baseCandidate)
val cancelSession = driver.request(case.moduleName)
check(driver.cancel(cancelSession) == DeliveryState.CANCELED)
check(case.moduleName !in driver.installedModules())
driver.injectNetworkFailure(true)
val failedSession = driver.request(case.moduleName)
check(driver.awaitTerminal(failedSession) == DeliveryState.FAILED)
check(case.moduleName !in driver.installedModules())
driver.injectNetworkFailure(false)
val retrySession = driver.request(case.moduleName)
check(driver.awaitTerminal(retrySession) == DeliveryState.INSTALLED)
check(driver.launchEntry(case.entryId))
driver.recreateProcess()
check(case.moduleName in driver.installedModules())
check(driver.launchEntry(case.entryId))
driver.upgradeTo(upgradeCandidate)
check(case.moduleName in driver.installedModules())
check(driver.launchEntry(case.entryId))
}
}签名、升级与回退要使用真实发布责任
Android 应用签名文档区分应用签名密钥、上传密钥、证书和 Play App Signing 的责任。动态功能回归需要知道最终设备 APK 由谁签名,而不是只确认 AAB 上传文件可接受。本地 bundletool set 若使用测试证书,只能证明对应测试身份下的安装与加载;测试轨道上的交付包应记录实际应用签名证书摘要。文档说明责任模型,但不能替任何候选确认签名正确。
升级矩阵至少包含两种前置状态:旧 base 已安装但 feature 未安装,以及旧 base 与 feature 都已安装。升级到新候选后,前者验证新版本按需请求和入口,后者验证已安装模块与新 base 的契约、资源和持久数据。还要覆盖升级过程中进程仍存活、升级后首次冷启动以及服务端配置变化。不能把卸载重装的成功结果当成保留数据升级通过。
回退要区分应用版本回退和业务能力关闭。商店分发通常不能依赖任意降 versionCode 覆盖安装,因此快速止损更适合使用服务端开关、隐藏入口、停止新请求或发布修复版本;已安装模块和本地数据如何处理需要产品契约。加固候选的回滚方案不能临时替换签名、从另一个 AAB 拼 split 或修改已签名文件。任何新候选都产生新身份并需要重新执行相应矩阵。
| 旧版状态 | 升级动作 | 必须验证 | 不能用什么替代 |
|---|---|---|---|
| 只有旧 base | 升级后首次请求 feature | 新 split 选择、安装与入口 | 全新安装成功 |
| 旧 base 加旧 feature | 覆盖升级到新候选 | 已装模块、资源与契约 | 未安装模块场景 |
| 活动下载会话 | 升级或进程重建 | 会话重查与安全 UI | 内存状态继续使用 |
| 旧数据库与模块数据 | 保留数据升级 | 迁移、读取和失败恢复 | 清空数据测试 |
| 能力被服务端关闭 | 模块已安装后进入 | 授权与回退页面 | 卸载模块作为授权 |
| 修复版本发布 | 新版本再次交付 | 新候选完整证据链 | 沿用旧候选结论 |
设备回执决定动态交付能否放行
Android instrumented tests 适合验证依赖真实 Android 运行时、组件、资源与系统 API 的行为。动态 feature 的设备测试应从模块未安装的 UI 或业务入口开始,记录请求到安装的状态,再进入真实功能;随后覆盖重启、进程恢复、配置变化和升级。测试用例要以最终用户可观察结果结束,而不是以 SDK 回调到达 installed 结束。
设备矩阵由实际交付条件和模块内容决定。至少按目标 API、ABI、密度、语言、安装来源、账号资格和旧版状态选代表组合;含 Native 库的 feature 扩展 ABI 覆盖,条件模块覆盖条件满足与不满足。单一设备、单一账号或本地模拟通过都不能外推。报告逐项列出执行、失败、未执行和阻塞原因,不用百分比或“全面兼容”掩盖空白。
最终放行记录应写成指定摘要 AAB 在列出的签名责任、device spec、测试轨道、设备和状态机路径下,派生集合符合预期且模块入口完成回归。它不能扩写为所有网络永久可用、所有设备兼容或加固不会影响任何模块。准备评估时,可整理最终 AAB、模块与交付模式、应用签名证书摘要、bundletool set、测试轨道版本、状态机回执和设备矩阵,再通过御盾中央平台提交申请。
| 证据 | 身份字段 | 结果字段 | 边界字段 |
|---|---|---|---|
| AAB 与构建 | 摘要、版本、模块和工具 | 结构检查 | 不等于设备交付 |
| APK set | set、device spec 和签名 | 选中 split 与安装结果 | 本地或轨道来源 |
| 安装会话 | 候选、设备、模块和 session | 状态序列、错误和最终集合 | 网络与账号条件 |
| 入口回归 | 模块、路由和输入 | 代码、资源和业务结果 | 仅覆盖列出路径 |
| 升级回归 | 旧版状态与新候选 | 已装和未装模块结果 | 不代表任意降级 |
| 放行记录 | 全部摘要与审批 | 通过、失败和未执行 | 禁止无证据外推 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 动态功能模块可以按安装时、条件或按需等生命周期交付,代码不一定随 base 首次存在。 | Play Feature Delivery 描述动态功能模块及其交付选项。 | 交付模式说明不证明加固后模块已经安装、加载或按预期保护。 |
| 按需模块需要测试请求、下载、安装、失败及本地测试路径。 | On-demand feature delivery testing 描述按需交付的测试与本地测试支持。 | 本地 Fake 管理器不经过真实 Play 分发,不能替代测试轨道回执。 |
| AAB 可以通过 bundletool 生成并安装适配目标设备的 APK set。 | Build and test Android App Bundles 描述本地构建 APK set、device spec 和设备安装流程。 | 本地派生只能证明列出工具、签名和设备条件,不能完全代表线上交付。 |
| bundletool 可以构建、检查和部署从 AAB 派生的 APK。 | bundletool 文档描述其命令、APK set 与签名参数。 | 未显式使用真实发布签名的本地产物不能被登记为生产候选。 |
| 依赖 Android 运行时、组件、资源和系统 API 的模块行为适合用设备端测试验证。 | Android instrumented tests 描述在设备或模拟器上运行并访问 Android 框架的测试。 | 单一设备通过不能代表全部 API、ABI、厂商和交付条件。 |
| 发布流程需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。 | Sign your Android app 描述 Android 签名与 Play App Signing 模型。 | 文档不能证明某个实际 AAB 或派生 split 使用了正确生产证书。 |
| 动态 feature 回归应同时覆盖未安装、活动会话、已安装、进程恢复和升级前置状态。 | 工程判断:这些状态具有不同文件集合、生命周期和失败恢复路径。 | 具体矩阵需要根据项目交付模式、入口、Native 依赖和目标设备调整。 |
| AAB、APK set、安装会话、入口回归和升级证据必须绑定同一候选链。 | 工程判断:只有可追溯身份才能排除重签、重打包、设备选择和版本替换造成的证据漂移。 | 静态摘要与状态日志一致仍不能替代真实业务入口和服务端授权测试。 |
工程常见问题
base APK 启动正常,能否说明动态功能模块兼容?
不能。按需或条件模块可能尚未交付,还要验证设备选择、请求状态机、安装后代码与资源入口、进程恢复和升级。
FakeSplitInstallManager 测试通过是否足够?
不够。它适合故障注入和业务分支测试,但不经过真实商店服务;还需 bundletool 设备回归和适用的 Play 测试轨道验证。
为什么要记录 device spec 和实际 split 集合?
AAB 会按设备 ABI、密度、语言、系统版本和交付条件选择不同文件。缺少这些输入,就无法重现某台设备收到的集合。
模块状态变为 installed 后还要测什么?
还要从真实入口验证类加载、资源、导航参数、Native ABI、业务授权、杀进程重进和失败回退,installed 不是业务验收结论。
升级测试为什么要区分模块已安装和未安装?
两者的设备文件集合和后续路径不同。已安装模块要验证与新 base 的契约,未安装模块要验证新版本首次请求与交付。
申请动态功能加固回归评估需要准备什么?
准备最终 AAB、模块与交付模式、应用签名证书摘要、device spec、APK set、测试轨道版本、状态机回执和设备矩阵,再通过御盾中央平台提交申请。