先看结论与判断条件
- 长稳测试应由可执行状态机定义阶段、动作、断言、超时和证据,单纯延长运行时间不会自动增加有效覆盖。
- 持续前台、前后台循环、受控系统回收观察和持久任务属于不同生命周期,应分阶段记录并允许单项失败。
- 每个阶段都要有业务断言,进程仍在或页面还能显示都不能替代数据正确、任务完成和资源可用。
- ANR 与异常退出必须通过 ApplicationExitInfo、trace、tombstone、日志和测试时间窗关联,不能只看最后一条自动化输出。
- WorkManager 调度被接受不代表任务按时执行或具备幂等性,约束、重试、配额和重复执行都要纳入回执。
- 云真机矩阵扩大型号和系统覆盖,但时长、容量与设备可用性有限,发布结论必须明确真实执行矩阵和未覆盖项。
长稳验收的核心是状态变化,不是挂钟时间
把 App 保持在一个静态页面数小时,只能证明该页面在当时设备条件下没有显性退出。它覆盖不了重复导航、缓存积累、前后台生命周期、Binder 重连、任务重试和系统资源变化。有效长稳测试应把时间用于重复真实状态转换,并在每次转换后检查业务结果和资源状态。
测试对象首先绑定 APK 摘要、versionCode、applicationId、签名、ABI 和加固配置摘要。设备记录型号、系统、内存、方向、locale、电源、网络与温度条件。任何候选或关键环境变化都要建立新 run_id,不能把不同设备或重新构建后的片段拼成一条连续时间线。
状态机至少定义阶段名称、进入条件、动作、最大持续时间、业务断言、允许事件、失败事件、采样点和退出条件。某个阶段超时应记录失败并保存现场,而不是继续跑完后只报告总时长。这样长稳结论能回答哪里稳定、哪里阻塞和哪里尚未覆盖。
| 维度 | 单纯计时 | 状态机验收 | 发布价值 |
|---|---|---|---|
| 操作 | 页面静置 | 重复真实动作 | 覆盖生命周期 |
| 断言 | 进程存在 | 阶段业务结果 | 发现静默错误 |
| 失败 | 最终是否退出 | 阶段级失败原因 | 可以定位 |
| 时间 | 总时长 | 阶段起止与超时 | 可复现 |
| 证据 | 截图或日志尾部 | 结构化事件时间线 | 可审计 |
| 结论 | 笼统稳定 | 已测范围和缺口 | 避免扩张 |
先设计阶段,再决定运行多长时间
一个实用序列可以从安装和冷启动开始,进入持续前台核心路径,再执行若干前后台循环,随后观察受控系统回收条件,验证持久任务,最后返回前台完成业务一致性检查。顺序不是固定模板,关键是每个阶段都有独立目的,且能在失败时保留之前已经完成的回执。
持续时间应来自业务周期和已知风险窗口,例如令牌刷新、长连接心跳、缓存淘汰、定时同步或媒体会话,而不是从一个整齐数字出发。测试计划记录为什么选择这个窗口,以及它没有覆盖哪些更长周期。没有依据的超长运行会消耗设备时间,却不一定触发新的行为。
每个阶段设置有界重试。设备暂时离线、自动化控件未出现和业务断言失败要使用不同错误码;只有明确属于基础设施的短暂错误才允许重试。业务失败不能通过重复直到偶然成功来抹掉,首次失败及后续结果都应保留。
| 阶段 | 主要动作 | 业务断言 | 出口 |
|---|---|---|---|
| 候选确认 | 核对安装与摘要 | 版本和签名匹配 | 身份一致 |
| 持续前台 | 循环核心路径 | 结果持续正确 | 达到计划窗口 |
| 前后台切换 | 离开并返回 | 会话和界面可用 | 循环完整 |
| 回收观察 | 进入受控系统条件 | 退出类型可解释 | 证据齐全 |
| 持久任务 | 满足或改变约束 | 任务状态符合契约 | 完成或有界失败 |
| 最终一致性 | 返回并查询状态 | 业务结果未静默损坏 | 生成回执 |
持续前台必须循环核心业务断言
前台阶段应选择会重复分配资源、访问 Native 代码、刷新数据或驱动渲染的真实核心路径。每轮记录开始、关键步骤、业务结果、耗时、进程和资源摘要。只做随机点击会增加路径数量,却很难复现;只做健康检查又可能绕开加固影响最大的类加载和 Native 入口。
断言要验证用户可观察结果和必要的内部一致性,例如列表内容对应固定数据集、媒体播放状态与时间推进一致、离线操作进入正确队列。页面未崩溃不代表返回值正确,自动化点击成功也不代表后台任务完成。每轮至少有一个能失败的业务断言。
失败后立即保存当前阶段、最近动作、截图、结构化日志、进程列表和系统诊断,再决定是否继续。若自动化脚本在异常页面继续点击,后续大量错误会掩盖首个故障。长稳测试的价值是捕获最早偏离,而不是生成最多红色日志。
| 层次 | 示例 | 证明内容 | 不足 |
|---|---|---|---|
| 自动化动作 | 点击或滑动成功 | 输入已发送 | 不证明业务正确 |
| 界面断言 | 目标状态可见 | 页面结果符合预期 | 不证明持久状态 |
| 业务断言 | 固定数据结果一致 | 核心逻辑可用 | 仅覆盖该路径 |
| 进程检查 | 目标进程存在 | 当前采样时存活 | 不证明无静默错误 |
| 资源摘要 | 内存和线程趋势 | 提供压力背景 | 不单独证明泄漏 |
| 系统诊断 | ANR 与退出记录 | 系统侧异常信号 | 需绑定时间窗 |
前后台切换要验证生命周期与会话连续性
前后台切换不是简单按 Home 再打开。测试要明确离开方式、后台停留条件、返回入口和目标 Activity,记录 onStop、后台任务、连接状态与恢复后的业务断言。通知跳转、最近任务列表、深链和直接启动入口可能产生不同返回路径,应按产品实际使用选择。
切后台期间系统可能限制网络、延迟任务、释放图形资源或降低进程重要性。加固候选返回前台后,要核对类加载、Native 句柄、会话凭据、Binder 连接、页面数据和待提交操作是否仍符合契约。不要为了测试绕过系统限制或强行常驻进程,那会改变真实环境。
循环次数和停留时间应覆盖快速切换与较长后台两个层次,并保持加固前后条件一致。若设备厂商有额外电源策略,应单独记录。一次成功返回不能代表重复切换稳定;一次系统回收也不能自动判失败,关键在于退出类型是否允许、重新进入后的业务断言是否满足。
| 检查点 | 离开前 | 后台期间 | 返回后 |
|---|---|---|---|
| 进程 | 记录 PID 与重要性 | 观察是否存活或退出 | 确认新旧进程身份 |
| 会话 | 记录非敏感状态标识 | 不主动绕过限制 | 验证授权语义 |
| Native 资源 | 记录功能可用 | 允许生命周期释放 | 重新调用并断言 |
| 网络 | 记录连接状态 | 观察系统约束 | 核对重连与失败语义 |
| 待办操作 | 固定测试任务 | 记录调度状态 | 检查完成或重试 |
| 界面 | 保存业务位置 | 不假设 Activity 存活 | 验证返回入口和结果 |
系统回收观察与人工终止必须分开
系统因资源压力回收后台进程、用户主动停止、应用自身退出、Java 崩溃、Native 崩溃和 ANR 是不同事件。长稳计划要预先列出允许与禁止的退出类型。人工终止可以验证某些重新进入路径,但不能冒充系统回收,也不能据此推断真实低内存行为。
ApplicationExitInfo 能提供退出原因、时间和相关诊断信息,部分系统版本还可返回 ANR trace 或 Native tombstone。采集时要按候选、进程、用户、时间窗和阶段关联。历史退出记录或另一个测试 run 的崩溃不能补到当前时间线,缺失记录也要说明系统与采集限制。
重新进入后应验证必要业务状态,但本篇重点是长稳阶段的存活、退出归属和业务连续性,不把所有状态恢复策略展开成另一套答案。若退出符合系统预期而业务断言失败,仍是当前阶段失败;若业务恢复正常但退出原因异常,也必须进入调查。
| 事件 | 是否预期 | 关键证据 | 判定 |
|---|---|---|---|
| 计划内正常停止 | 按用例定义 | 阶段和回调 | 检查清理 |
| 系统资源回收 | 部分阶段允许 | exit reason 与重要性 | 检查返回业务 |
| Java 崩溃 | 不允许 | 堆栈与候选身份 | 阶段失败 |
| Native 崩溃 | 不允许 | tombstone 与匹配符号 | 阶段失败 |
| ANR | 不允许 | trace 与时间线 | 阶段失败 |
| 基础设施断连 | 非业务结论 | 设备和测试日志 | 阻塞并复核 |
持久任务要验证约束、重试和幂等
Android persistent work 文档说明,WorkManager 的持久化调度可跨进程退出和设备重启保存任务,并受约束、重试和系统配额影响。将 WorkRequest 成功入队只证明调度请求被接受,不代表任务会立即运行、按预想时刻完成或业务操作天然幂等。
长稳状态机应记录任务唯一标识、入队时间、约束、attempt、开始、结束、结果和业务副作用摘要。测试要覆盖约束满足、约束暂不满足、任务重试、应用进程离开和再次进入。若业务操作可能重复提交,先提供幂等键或事务边界,再验证系统重试不会造成重复结果。
测试不应通过不断轮询或持有前台来强迫任务执行,那会绕过真实调度条件。超时后保存当前 WorkInfo、设备约束、系统日志和时间线,区分尚未调度、正在运行、重试等待、取消和失败。云真机的会话时长可能不足以覆盖长约束窗口,必须把这类用例放到可持续设备。
用状态机脚本汇总阶段回执
下面的 Python 脚本读取一份公开安全的阶段计划和一份 JSON Lines 事件流。计划按顺序列出 stage 与 required_assertion,事件记录阶段、时间、进程存活、业务断言、ANR 和退出原因。脚本拒绝未知阶段、时间倒序、缺失事件和非法断言状态,并为每个阶段生成通过或失败摘要。
示例要求阶段至少出现一次业务断言,任何 fail、ANR 或非空退出原因都进入 issues。真实项目可以在计划中为某些阶段声明允许的退出原因,但允许项必须显式、有限并经过审批,不能在聚合后临时忽略。脚本不控制设备、不执行终止操作,也不读取用户数据。
事件流应在每个阶段完成时原子落盘,并与 APK、设备和 run_id 绑定。自动化进程意外终止时,已经完成的阶段仍可审计,未完成阶段明确为 missing。最终摘要不是稳定性证明本身,只有当其原始事件、系统诊断和测试条件都可复核时,才能作为发布回执。
import json
import sys
from collections import defaultdict
from pathlib import Path
if len(sys.argv) != 3:
raise SystemExit('usage: soak_summary.py plan.json events.jsonl')
plan_path = Path(sys.argv[1])
events_path = Path(sys.argv[2])
for path in (plan_path, events_path):
if not path.is_file():
raise SystemExit(f'missing input: {path.name}')
plan = json.loads(plan_path.read_text(encoding='utf-8'))
stages = plan.get('stages')
if not isinstance(stages, list) or not stages:
raise SystemExit('plan stages are missing')
ordered = []
for item in stages:
name = str(item.get('stage', '')).strip()
assertion = str(item.get('required_assertion', '')).strip()
if not name or not assertion or name in ordered:
raise SystemExit('invalid or duplicate stage')
ordered.append(name)
events = defaultdict(list)
last_timestamp = -1
for line_number, line in enumerate(events_path.read_text(encoding='utf-8').splitlines(), 1):
if not line.strip():
continue
row = json.loads(line)
stage = str(row.get('stage', ''))
timestamp = row.get('timestamp_ms')
if stage not in ordered:
raise SystemExit(f'unknown stage at line {line_number}')
if not isinstance(timestamp, int) or timestamp <= last_timestamp:
raise SystemExit(f'non-monotonic timestamp at line {line_number}')
if row.get('assertion') not in ('pass', 'fail'):
raise SystemExit(f'invalid assertion at line {line_number}')
last_timestamp = timestamp
events[stage].append(row)
summary = []
for stage in ordered:
rows = events.get(stage, [])
if not rows:
raise SystemExit(f'missing events for stage {stage}')
issues = []
if any(row['assertion'] == 'fail' for row in rows):
issues.append('business-assertion-failed')
if any(row.get('anr') is True for row in rows):
issues.append('anr-observed')
exits = sorted({str(row['exit_reason']) for row in rows if row.get('exit_reason')})
if exits:
issues.append('process-exit-observed')
alive_samples = sum(1 for row in rows if row.get('alive') is True)
summary.append({'stage': stage, 'events': len(rows), 'alive_samples': alive_samples, 'exit_reasons': exits, 'issues': issues, 'status': 'pass' if not issues else 'fail'})
print(json.dumps(summary, ensure_ascii=False, indent=2))ANR 定位要看主线程之前发生了什么
Android ANR 诊断需要区分主线程阻塞、锁竞争、Binder、I/O 和组件超时。长稳运行中,资源逐渐积累或后台重连可能让一次普通调用最终跨过超时边界。trace 顶部看到等待并不一定是最初原因,需要对齐之前的锁持有、跨进程调用和 I/O 时间线。
测试应在阶段边界采集轻量健康信号,ANR 发生后保存 trace、ApplicationExitInfo、进程与线程、最近业务动作和候选身份。不要为了持续运行而自动关闭无响应对话框后继续计为通过;这会丢失首次故障现场,并让后续状态不可解释。
异常没有在本地矩阵复现时,可以参考 Android vitals 的线上 ANR、崩溃、启动和设备分布信号,但要遵守其安装来源、用户同意和统计口径限制。线上聚合用于发现设备族与版本线索,不能替代同候选的受控复现,也不能单凭趋势宣称加固因果。
设备矩阵、时长和发布结论必须诚实
Firebase Test Lab Android matrices 说明矩阵由型号、OS、方向和 locale 等维度组成,任一执行失败都会影响矩阵结果。长稳任务还受到云会话时长、设备容量和基础设施稳定性约束。适合云端的短状态循环可以扩展覆盖,超出会话能力的持续任务应放在专用代表设备。
Android instrumented tests 适合驱动真实运行时、组件和系统 API 路径,但单一设备通过不能代表全部 API、ABI 与厂商组合。矩阵选择应依据最低支持范围、真实用户分布和已知风险,不按可用设备随意抽样。每个执行保留实际型号、OS、方向、locale、时长与基础设施状态。
准备长稳与后台切换评估时,应提交候选摘要、状态机计划、业务断言、设备矩阵、原始事件、WorkManager 回执、ApplicationExitInfo、ANR 与 Native 诊断以及未覆盖窗口。申请入口由御盾中央平台统一承接。没有真实执行和 HTTP 无关,本地计划或脚本通过不能写成长稳兼容、无异常或性能改善。
| 环境 | 适合 | 主要限制 | 回执重点 |
|---|---|---|---|
| 本地开发设备 | 快速定位和短循环 | 代表性有限 | 工具与设备状态 |
| 专用代表设备 | 长时间持续运行 | 数量有限 | 完整时间线 |
| 云真机矩阵 | 型号和系统扩展 | 会话时长与容量 | 实际矩阵结果 |
| instrumented test | 可重复组件路径 | 用例外路径缺失 | 断言与失败现场 |
| 线上 vitals | 真实用户趋势 | 样本与口径限制 | 版本和设备分布 |
| 人工探索 | 发现未知交互 | 重复性较弱 | 转化为固定用例 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| ApplicationExitInfo 可以提供进程退出原因和相关诊断信息。 | Android ApplicationExitInfo 说明退出原因、ANR trace 与新版本 Native tombstone 能力。 | 退出记录必须与同一候选、进程、时间窗和用户路径关联。 |
| Android vitals 提供崩溃、ANR、启动和设备分布等线上质量信号。 | Android vitals 说明 Play 侧质量指标及其观察范围。 | 样本受安装来源、用户同意和统计口径限制,不能代替发布前长稳回归。 |
| ANR 定位需要区分主线程、锁、Binder、I/O 和组件超时。 | Diagnose Android ANRs 给出 ANR 类型与排查方法。 | trace 表象堆栈不一定是最初阻塞源,需要结合阶段时间线。 |
| WorkManager 持久任务会受约束、重试与系统配额影响。 | Android persistent work 说明持久任务的保存、约束和执行语义。 | 请求入队不证明任务按时完成,也不证明业务操作具备幂等性。 |
| 云测试矩阵由设备型号、OS、方向和 locale 等维度构成。 | Firebase Test Lab Android matrices 说明 Android 矩阵配置与结果关系。 | 云矩阵仍需依据真实用户和最低支持范围选择,并受时长与容量限制。 |
| 依赖真实 Android 运行时和组件的路径应通过设备端测试。 | Android instrumented tests 说明测试可在真实或模拟 Android 环境执行。 | 单一设备、ABI 和 API 版本通过不能代表完整支持矩阵。 |
| 长稳验收必须给每个阶段设置业务断言和独立失败结果。 | 工程判断:进程存活与总时长不能发现静默数据错误和局部阶段失败。 | 阶段通过只覆盖已执行动作、设备与时间窗口,不能扩张到未知路径。 |
| 云真机时长不足的用例应转移到可持续的代表设备。 | 工程判断:测试在阶段完成前被基础设施终止,无法形成完整长稳回执。 | 专用设备提升持续时间,不自动提升型号覆盖或证明线上稳定。 |
工程常见问题
App 连续运行很久没有崩溃,能否算长稳通过?
不能只凭总时长。需要状态机覆盖真实动作、前后台、任务和退出观察,并在每个阶段保存业务断言与诊断证据。
前后台切换测试只按 Home 再打开是否足够?
不足。应定义离开方式、后台条件、返回入口、进程身份、会话、Native 资源、网络和业务结果,并覆盖重复切换。
WorkManager 入队成功是否代表后台任务正常?
不代表。还要观察约束、attempt、开始、结果、重试、配额和业务副作用,并验证重复执行的幂等性。
系统回收进程是否应直接判定失败?
取决于阶段契约。先用 ApplicationExitInfo 确认退出类型,再检查重新进入后的业务断言;异常退出和静默损坏仍应失败。
云真机矩阵能否替代专用长稳设备?
通常不能完全替代。云矩阵适合扩展短场景覆盖,长约束窗口和持续运行需考虑会话时长、容量与基础设施中断。
申请加固后长稳评估需要准备什么?
准备候选摘要、状态机、业务断言、设备矩阵、原始事件、持久任务回执、退出记录、ANR 与 Native 诊断和缺口,再通过御盾中央平台提交。