跳转至

移动端 App 测试要点

移动端测试和 Web 最大的区别是:你的进程随时可能被杀,你的权限随时可能被收回,你的网络随时会掉。 这些不是异常场景,是日常场景。

下面的命令在 Pixel 7(Android 14)、iPhone 15(iOS 17.2)、adb 35.0.2、Xcode 15.2 上验证过;未特别说明时包名为 com.qalab.app。

一、安装、升级与降级

安装矩阵

场景 Android 命令 iOS 对应 必查项
首次安装 adb install app-release.apk TestFlight / xcrun devicectl 冷启动耗时、首屏、权限弹窗时机
覆盖安装 adb install -r app-release.apk 同 Bundle ID 覆盖 登录态、本地数据、桌面快捷方式
保留数据卸载 adb shell pm uninstall -k com.qalab.app 无对应能力 -k 保留 /data/data 与外部存储
彻底卸载重装 adb uninstall com.qalab.app 长按删除 App 残留文件、Keychain、SD 卡目录
降级安装 adb install -d -r app-old.apk 无法降级,只能删后重装 数据库 schema、缓存格式
跨大版本升级 装 v1.8 后用 v2.3 覆盖 App Store 升级 迁移脚本、埋点版本号
存储不足 填满磁盘后安装 同 失败提示是否可理解

真实安装失败码,看到就要能定位:

INSTALL_FAILED_VERSION_DOWNGRADE     # 没加 -d,或 versionCode 低于已安装版本
INSTALL_FAILED_UPDATE_INCOMPATIBLE   # 签名不一致,测试包与正式包混装
INSTALL_FAILED_INSUFFICIENT_STORAGE  # 空间不足
INSTALL_PARSE_FAILED_NO_CERTIFICATES # APK 未签名或被二次篡改
INSTALL_FAILED_TEST_ONLY             # AndroidManifest 里 testOnly="true"

testOnly 这一条尤其常见:Android Studio 直接 Run 出来的包带 android:testOnly="true",用 adb install 装到另一台机器就报 INSTALL_FAILED_TEST_ONLY。要么加 adb install -t,要么用 assembleRelease 出包。

# 查版本与签名,确认装的是哪个包
adb shell dumpsys package com.qalab.app | grep -E 'versionName|versionCode|firstInstallTime|lastUpdateTime'
adb shell pm list packages -3 | grep qalab
apksigner verify --print-certs app-release.apk

升级要做的事

升级测试不是“能打开就行”,要逐项确认数据迁移:

  1. 登录态是否保留(token 存储 key 有没有改名)。
  2. 本地数据库是否迁移(Room 的 Migration、iOS 的 CoreData 轻量迁移)。
  3. 本地缓存里的旧结构能否被新代码解析。
  4. 上次未提交的草稿、购物车、离线队列是否还在。
  5. 旧版本遗留的权限、通知渠道是否仍然有效。
  6. 桌面小组件、快捷方式、通知点击是否还能拉起。

案例 1:降级安装后启动即崩。 测试为了复现旧版问题,用 adb install -d -r 把 v2.3 降到 v1.8,启动直接崩溃:

java.lang.IllegalStateException: Room cannot verify the data integrity.
Looks like you've changed schema but forgot to update the version number.

根因是 v2.3 给表加了列并把 user_version 提到 12,v1.8 的 Room 只认 11,检测到 schema hash 不匹配就自杀。这不是“降级”本身的问题,而是降级后仍读着新版数据库。正确做法是降级前 adb shell pm clear com.qalab.app,或在 Application 启动时捕获该异常并重建本地库(同时上报,避免静默丢数据)。

二、权限

Android 运行时权限的三个状态

状态 checkSelfPermission shouldShowRequestPermissionRationale 再次申请会弹窗
未申请 DENIED false 是
拒绝过一次 DENIED true 是
永久拒绝 DENIED false 否,直接回调拒绝

Android 11 起,用户连续拒绝两次即被视为“不再询问”,系统不再弹窗。所以“首次拒绝后再申请”的用例必须写成两条:拒绝一次后申请(应弹窗 + 应显示 rationale)、拒绝两次后申请(不弹窗,应引导去设置)。

// 判定是否该展示“去设置开启”引导
fun shouldGuideToSettings(activity: Activity, perm: String): Boolean =
    !ActivityCompat.shouldShowRequestPermissionRationale(activity, perm) &&
    ContextCompat.checkSelfPermission(activity, perm) != PackageManager.PERMISSION_GRANTED

注意:首次安装、从未申请过时 shouldShowRequestPermissionRationale 同样返回 false,不能据此判断“永久拒绝”。必须先记录“是否已申请过”,再结合返回值判断。

# 查看与修改权限状态(调试用)
adb shell dumpsys package com.qalab.app | grep -A 20 'runtime permissions'
adb shell pm grant com.qalab.app android.permission.CAMERA
adb shell pm revoke com.qalab.app android.permission.CAMERA
adb shell appops set com.qalab.app CAMERA deny     # 模拟“拒绝但不再询问”

iOS 权限的特殊性

iOS 的权限弹窗每个 App 每次安装只弹一次。用户点了“不允许”,代码再调 requestAuthorization 不会弹窗,直接回调 denied。所以“二次申请”在 iOS 上不是再弹一次,而是必须把用户送到“设置 → 隐私与安全性 → 相机”。

AVCaptureDevice.requestAccess(for: .video) { granted in
    if !granted {
        // 无法再次弹窗,只能引导
        DispatchQueue.main.async { self.showSettingsAlert() }
    }
}
# 模拟器上重置/预置权限,真机不支持
xcrun simctl privacy booted reset all com.qalab.app
xcrun simctl privacy booted grant photos com.qalab.app
xcrun simctl privacy booted revoke camera com.qalab.app

通知权限更特殊:调 requestAuthorization 会弹窗;但用 UNAuthorizationOptions.provisional(临时授权)不弹窗,通知静默进通知中心,用户“保留”后才转为正式授权。测试要覆盖三条路径:允许、拒绝、临时授权后升级。

案例 2:永久拒绝后点“拍照”无任何反应。 用户在 Android 13 上拒绝了两次相机权限,之后每次点“拍照”按钮都没有 toast、没有跳转,日志里只有一行 onRequestPermissionsResult: DENIED。产品预期是“引导去设置”。根因是回调里只处理了 GRANTED,DENIED 分支空实现。修复时同时补了 shouldShowRequestPermissionRationale 判断,把“首次拒绝”和“永久拒绝”分成两套文案,并且设置页跳转要用 Settings.ACTION_APPLICATION_DETAILS_SETTINGS 带上包名,否则会落到应用列表。

三、生命周期:前后台、锁屏、杀进程

操作 命令 关注点
退到后台 adb shell input keyevent KEYCODE_HOME onStop 是否停掉定时器、传感器、播放
回到前台 adb shell am start -n com.qalab.app/.MainActivity 数据是否刷新、是否重复弹权限
锁屏 adb shell input keyevent KEYCODE_POWER 音视频是否暂停、计时是否继续
杀后台进程 adb shell am kill com.qalab.app 只杀后台,模拟系统回收
强制停止 adb shell am force-stop com.qalab.app 等价于用户在设置里“强行停止”
内存紧张 adb shell am send-trim-memory com.qalab.app RUNNING_LOW onTrimMemory 是否释放图片缓存

am kill 与 force-stop 的差别很容易混淆:am kill 只能杀“可被杀”的后台进程,不改变 App 的停止状态;force-stop 会把包标记为 stopped,之后广播、闹钟、推送都收不到,直到用户再次手动启动。

进程死亡恢复

Android 在后台内存不足时会直接杀进程。回到前台时系统重建 Activity,并回放 onSaveInstanceState 的 Bundle;如果没存,savedInstanceState == null。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 进程死亡重建时这里为 null,不能假设用户是首次进入
    val restored = savedInstanceState?.getString("draft")
    if (restored == null && isComingFromRecents()) showDraftLostTip()
}

iOS 对应的是 UIApplicationDelegate 的 state restoration 或 SwiftUI 的 @SceneStorage。没做恢复的页面,用户切回来看到的是空列表 + 空搜索框,比崩溃更让人困惑。

案例 3:从后台回来变成白屏。 某列表页把数据存在 ViewModel 里,进程被回收后重建,ViewModel 重新构造、请求还没回来,页面渲染了一个 null 的 adapter,白屏 2 秒。根因是 onCreate 里默认 savedInstanceState == null 就是首次启动,跳过 loading 态。修复是显式区分“进程死亡恢复”和“首次启动”,恢复时先渲染骨架屏并重新拉取。

另外要测后台停留时长:Android 的 Doze 会在息屏一段时间后限制网络与闹钟,iOS 会挂起 App。后台 30 分钟再回来,检查: - 登录 token 是否已过期,是否有静默续期。 - 列表是否还是 30 分钟前的数据。 - 上传/下载任务是否被中断,恢复后是否续传。 - 推送角标与实际未读数是否一致。

四、弱网与网络切换

三种限速方式

Android 模拟器(AVD)自带网络整形:

adb emu network status            # 查看当前延迟与速率
adb emu network delay 3000        # 3s 延迟
adb emu network speed gsm         # 上行/下行约 40/14 kbps 级别
adb emu network speed full        # 恢复

启动时也可以直接指定:emulator -avd Pixel_7_API_34 -netdelay gsm -netspeed gsm。

iOS 用 Network Link Conditioner(需装 Additional Tools for Xcode),内置 3G、Edge、LTE、DNS 慢、100% 丢包等配置,作用于整机,比在 App 里改超时更接近真实。

Charles 的 Throttle 适合精确定位某个接口:Proxy → Throttle Settings → 勾选 Enable Throttling,预设 3G(带宽约 780 kbps、延迟 100 ms、利用率 50%),也可以只对 api.example.com 生效。配合 Breakpoints 可以让指定请求永不返回,验证超时分支。

判定标准

弱网不是“看着能转圈就行”,要有明确阈值:

场景 预期行为 判定标准
3s 延迟 显示 loading,可取消 3s 内不出现“网络错误”
请求超时 超时时间 ≤ 15s,有重试入口 OkHttp 默认 10s connect / 10s read
断网 立即提示,走本地缓存 有网后自动恢复,不需重启
丢包 30% 重试有退避,不放大流量 重试间隔指数增长,最多 3 次
Wi-Fi ↔ 4G 请求不中断或快速失败重连 不出现半小时无响应

真实报错长这样,看到要能对应到配置项:

java.net.SocketTimeoutException: failed to connect to /10.0.2.2 (port 8080) after 10000ms
java.io.IOException: canceled due to timeout
javax.net.ssl.SSLHandshakeException: Connection closed by peer

第三条在弱网 + 代理场景很常见:Charles 证书没装到系统信任区,或 TLS 版本被中间设备掐断,不要一律当“网络差”处理。

案例 4:弱网重试造成重复下单。 下单接口 10s 超时,客户端失败后自动重试 3 次,用户在 3G 下点了两次,最终生成 4 笔订单。根因是重试没有幂等键,服务端也没做去重。修复是客户端生成 requestId 放进 header,服务端按 requestId 唯一索引,重复请求返回首次结果。验证时用 Charles 把响应延迟到 12s,同时在客户端断开,观察只落一单。

五、中断场景

中断 模拟命令 检查点
来电 adb emu gsm call 13800138000 音视频是否暂停、页面是否被遮挡、挂断后是否恢复
短信 adb emu sms send 10086 "test" 通知展示、点击跳转
耳机插入/拔出 手动,或 adb shell input keyevent 79 音频路由切换,拔出后是否外放
低电量 adb shell dumpsys battery set level 5 是否触发省电策略、是否限制后台
充电 adb shell dumpsys battery set ac 1 充电提示、动画
闹钟/日历提醒 系统时钟设置 全屏 Intent 是否打断
分屏/画中画 手动拖拽 布局是否重排、视频是否续播
系统弹窗 权限、低存储、系统更新 回来后页面状态
adb emu gsm call 13800138000
adb emu gsm cancel 13800138000
adb emu sms send 10086 "hello"
adb shell dumpsys battery set level 5
adb shell dumpsys battery reset

低电量测试经常被忽略的一点:Android 的省电模式和 iOS 的低电量模式都会限制后台刷新与自动播放。iOS 低电量模式下视频自动播放会直接被拒,报 NotAllowedError,这不是 bug,但产品文案必须能解释。

六、深链与推送

深链

# Android App Links / 自定义 scheme
adb shell am start -W -a android.intent.action.VIEW \
  -d "https://example.com/detail/88" com.qalab.app
adb shell am start -W -a android.intent.action.VIEW \
  -d "qalab://detail?id=88" com.qalab.app

# 查看 App Links 验证状态(Android 12+)
adb shell pm get-app-links com.qalab.app
adb shell pm set-app-links --package com.qalab.app 1 all
# iOS 模拟器打开 Universal Link
xcrun simctl openurl booted "https://example.com/detail/88"

深链要测的组合:

  1. App 未安装 → 落地页/H5 兜底,不能白屏。
  2. App 已安装未启动 → 冷启动,参数从 getIntent() / application(_:open:options:) 进入。
  3. App 在后台 → 热启动,走 onNewIntent,此时 Activity 不会重建,如果只在 onCreate 解析参数就会丢。
  4. 已登录 / 未登录 → 未登录先跳登录,登录后必须回到原目标页(参数要暂存)。
  5. 参数非法/缺失 → 降级到首页,不崩溃。
  6. 重复点击同一链接 → 不叠加多个页面实例。

Android 12 起 App Links 默认不再自动打开,必须验证通过(assetlinks.json 可访问、签名指纹匹配)或用户手动允许,否则链接会落到浏览器。这是“测试环境好好的,线上全进浏览器”的典型原因。

curl -s https://example.com/.well-known/assetlinks.json | jq .
curl -s https://example.com/.well-known/apple-app-site-association | jq .

推送

检查项 Android iOS
权限 Android 13+ 需 POST_NOTIFICATIONS 运行时权限 首次启动请求,或 provisional
通道 Android 8+ 必须建 NotificationChannel 无通道概念,有中断级别
离线 厂商通道(小米/华为/OPPO/vivo) APNs
点击 PendingIntent 携带参数,冷启动要能取到 didReceive response 取 userInfo
角标 需 Launcher 支持 applicationIconBadgeNumber
# 查看通知是否真的发出、通道是否被关闭
adb shell dumpsys notification --noredact | grep -A 8 com.qalab.app
# 勿扰模式状态:0=关闭 1=仅优先 2=完全静音 3=仅闹钟
adb shell settings get global zen_mode

要覆盖的推送场景:前台收到(是否展示横幅)、后台收到、杀进程后收到、通知权限关闭、通道被用户关闭、点击后冷启动、点击后热启动、多条聚合、免打扰模式。厂商通道的离线推送必须在真机上测,模拟器只走 FCM,测不出国内 ROM 的保活与限制。

七、本地存储与清缓存

位置 内容 清除方式
Android shared_prefs/ 登录 token、设置项 清除数据 / pm clear
Android databases/ Room/SQLite 清除数据
Android cacheDir 图片、H5 离线包 应用内“清除缓存”
Android 外部私有目录 下载文件、日志 卸载时删除(getExternalFilesDir)
iOS Library/Caches 图片、WebView 缓存 系统“清除缓存”或应用内
iOS Documents 用户数据、离线文件 删除 App
iOS Keychain token、设备标识 卸载不删除

案例 5:iOS 卸载重装后自动登录到别人的账号。 测试机上 A 账号登录后卸载 App,重装打开直接进了 A 的首页,而此时输入框里是 B 的手机号。根因是 token 存在 Keychain,iOS 卸载 App 不会清 Keychain,新安装读到了旧 token,服务端也认。修复是启动时校验“Keychain token 的 userId”与“当前设备绑定信息”,并在首次启动写入安装标识,不一致就强制登出。这个坑在共享测试机上极易造成“数据串号”,且很难复现。

# Android 查看应用私有数据(debuggable 包)
adb shell run-as com.qalab.app ls -R /data/data/com.qalab.app
adb shell run-as com.qalab.app cat shared_prefs/config.xml

# 清数据 vs 清缓存
adb shell pm clear com.qalab.app                  # 全清,等价于重装
adb shell rm -rf /data/data/com.qalab.app/cache  # 仅清缓存(需 root/run-as)
# iOS 模拟器查看沙盒路径
xcrun simctl get_app_container booted com.qalab.app data

“清除缓存”按钮的验收标准:清完后登录态仍在、首页能打开、图片重新下载;如果清完要求重新登录,说明缓存和凭证没分开存,属于实现缺陷。

八、耗电、内存与流量

耗电

adb shell dumpsys batterystats --reset
# 执行 30 分钟典型场景(后台待机 + 前台浏览 + 推送)
adb shell dumpsys batterystats > batterystats.txt
adb shell dumpsys batterystats --charged com.qalab.app > charge.txt

把 batterystats.txt 丢进 Battery Historian(docker run -p 9999:9999 gcr.io/android-battery-historian/stable:3.0 --port 9999)可以看到唤醒锁、GPS、网络、JobScheduler 的时间线。重点看三件事:是否有长持有 WakeLock、是否有高频定位、息屏后是否有周期性唤醒。

iOS 用 Xcode 的 Energy Log,或设置 → 电池 → 按 App 查看“后台活动”。后台 8 小时耗电超过 5% 就要查后台任务与定位。

内存

adb shell dumpsys meminfo com.qalab.app
adb shell dumpsys meminfo com.qalab.app -d     # 看到具体 Dalvik 对象

关注 TOTAL PSS。反复进入退出同一页面 10 次,PSS 持续上涨且不回落,基本就是泄漏。iOS 用 Instruments 的 Allocations + Leaks,或 Xcode Debug Memory Graph 看循环引用。

流量

adb shell dumpsys netstats detail | grep -i uid
adb shell dumpsys package com.qalab.app | grep -i uid

更直观的方式是 Charles 统计单次会话流量,或用 Android Studio Network Profiler。判定标准要写死:首页冷启动流量 ≤ 500KB、列表翻页 ≤ 200KB/页、图片默认走 WebP、视频只在 Wi-Fi 预加载。

九、机型适配

维度 典型问题 验证方式
屏幕密度 1px 边框在 3x 下消失或过粗 用 dp 而非 px,真机看截图
刘海/挖孔 顶部内容被遮挡 WindowInsets / safeAreaInsets
折叠屏 展开后布局拉伸、重启 Activity android:resizeableActivity="true"
平板/大屏 列表一列拉满 1200dp sw600dp 布局,限制内容最大宽度
低端机 动画掉帧、启动 5s+ 2GB 内存机型实测
厂商 ROM 后台被杀、权限被二次管控 小米/华为/OPPO 真机各一台
字体缩放 系统字体调大后文字截断 设置最大字号,检查按钮与列表
深色模式 硬编码白色背景刺眼 切换系统深色模式逐页看

厂商 ROM 的额外权限要单独测:小米的“自启动”、华为的“应用启动管理”、OPPO 的“允许后台运行”。这些不在标准权限模型里,用户关了之后推送收不到、后台任务不执行,但 App 内没有任何提示。

十、iOS 与 Android 的关键差异

维度 Android iOS 测试影响
返回 系统返回键/手势 无全局返回,靠导航栏 Android 要测物理返回是否退出页面
权限 可反复申请,二次即永久拒绝 每次安装只弹一次 引导文案与跳转逻辑不同
后台 进程可长期存活,受 Doze 限制 进入后台很快被挂起 后台任务、上传、定位策略不同
推送 FCM + 厂商通道 仅 APNs 国内 Android 离线到达率要单独验
文件 可直接读写外部存储 沙盒 + Keychain 跨卸载保留 清缓存/重装行为不同
剪贴板 读取无提示(Android 12+ 有 toast) iOS 16+ 读取弹“允许粘贴” 自动化脚本会被弹窗打断
追踪 无 ATT iOS 14.5+ ATT 弹窗 广告归因、IDFA 获取
日期格式 跟随 locale 跟随 locale,且解析更严格 跨端展示不一致

十一、隐私合规

监管检查的不是“有没有隐私政策”,而是收集的时机、范围、告知是否一致。测试要按下面的顺序逐项过:

  1. 首次启动是否在任何 SDK 初始化之前弹出隐私政策,用户同意后才初始化统计、推送、广告 SDK。
  2. 拒绝隐私政策后,App 是否仍可浏览基础内容,且不产生任何网络采集请求(用 Charles 抓包验证)。
  3. 申请的权限是否与当前功能匹配:不点拍照就不该申请相机,不进地图就不该申请定位。
  4. 是否存在超范围收集:IMEI、IMSI、MAC、Android ID、应用列表、剪贴板。
  5. 隐私政策里列出的第三方 SDK,是否与实际打包的 SDK 一致(./gradlew dependencies 或解包看 libs/)。
  6. 是否提供账号注销、个人信息导出/删除入口,且真的生效。
  7. 定向推送是否有明显关闭入口。
  8. 未成年人模式、实名认证是否符合应用商店要求。
# 看实际打进去的 SDK 与权限
adb shell dumpsys package com.qalab.app | grep -E 'requested permissions|declared permissions'
./gradlew :app:dependencies --configuration releaseRuntimeClasspath | grep -i -E 'umeng|jpush|bugly'

常见违规点:把 READ_PHONE_STATE 写进 Manifest 却从不使用;隐私弹窗点“拒绝”后仍初始化推送 SDK;SDK 在 Application.onCreate 里自启动并读取设备信息。这些在应用商店审核和监管抽检中都是硬伤。

十二、崩溃取证

Android

# 实时看崩溃(只看 crash buffer,避免被业务日志淹没)
adb logcat -b crash -d
adb logcat -b crash -v threadtime

# 系统 dropbox 里的崩溃记录
adb shell dumpsys dropbox --print data_app_crash

# native 崩溃
adb shell ls /data/tombstones/
adb shell cat /data/tombstones/tombstone_00

# ANR
adb shell ls /data/anr/
adb shell cat /data/anr/traces.txt

Java 崩溃栈直接可读;native 崩溃要符号化:

ndk-stack -sym app/build/intermediates/merged_native_libs/release/out/lib/arm64-v8a \
          -dump crash.txt

Release 包的 Java 堆栈是混淆的,需要 mapping 文件:

retrace.sh -verbose mapping.txt obfuscated_stack.txt

iOS

# 找到崩溃报告
ls ~/Library/Logs/DiagnosticReports/*.ips

# 用 dSYM 符号化单个地址
atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp \
     -arch arm64 -l 0x104b40000 0x104b4a2c0

# 确认 dSYM 与二进制 UUID 匹配
dwarfdump --uuid MyApp.app/MyApp
dwarfdump --uuid MyApp.app.dSYM

# 旧脚本仍可用,Xcode 15 起不推荐(对 .ips 支持差)
./symbolicatecrash MyApp.crash MyApp.app.dSYM > symbolicated.crash

现在 Xcode 15 导出的崩溃是 .ips JSON 格式,symbolicatecrash 经常解析失败,优先用 Xcode → Window → Organizer → Crashes,或直接把 .ips 与 dSYM 一起上传到崩溃平台。手工符号化只作为最后手段。

UUID 不匹配是最常见的“符号化失败”原因:CI 每次构建都会生成新 UUID,dSYM 没归档就永久丢失。上线包必须把 dSYM 上传到崩溃平台,并在构建流水线里校验。

案例 6:崩溃率 0.3% 但无法定位。 线上崩溃平台只显示 SIGSEGV 和一堆十六进制地址。查下来是 CI 用 strip 去掉了符号且未保留 dSYM,只能等下一版复现。之后在流水线里加了 dwarfdump --uuid 与 dSYM 上传校验,并把 DEBUG_INFORMATION_FORMAT 设为 DWARF with dSYM File。

十三、Monkey 稳定性测试

# 基础随机事件流
adb shell monkey -p com.qalab.app -v 500

# 可控节奏 + 事件配比 + 固定种子(可复现)
adb shell monkey -p com.qalab.app \
  --throttle 300 \
  --pct-touch 50 --pct-motion 20 --pct-nav 10 --pct-majornav 5 \
  --ignore-crashes --ignore-timeouts --monitor-native-crashes \
  -s 20261022 -v -v 5000
参数 作用 建议值
--throttle 事件间隔 ms 300,太快会把人造 ANR 当缺陷
-s 随机种子 固定值,便于复现
--pct-touch 点击占比 50
--ignore-crashes 崩溃后继续 压测时开启
--ignore-timeouts ANR 后继续 压测时开启
--monitor-native-crashes 监控 native 崩溃 必开
--pkg-whitelist 只测指定包 等价 -p

跑完要检查三件事:/data/anr/ 是否新增、/data/tombstones/ 是否新增、dumpsys dropbox 是否有 data_app_crash。Monkey 只覆盖“随机点击不崩”,不覆盖业务正确性,别拿它当功能测试。

十四、验收清单

类别 验收项 判定
安装 首次/覆盖/降级/空间不足 均有明确结果,无静默失败
升级 登录态、数据库、草稿、缓存 数据无丢失,迁移可回滚
权限 允许/拒绝一次/永久拒绝/设置页返回 每条路径都有下一步
生命周期 后台 30 分钟、锁屏、杀进程、恢复 不白屏、不重复提交、状态可恢复
弱网 3s 延迟、断网、丢包、切换网络 有 loading、有超时、有重试
中断 来电、短信、耳机、低电量 中断可恢复,音频路由正确
深链 未安装/冷启/热启/未登录/非法参数 均落到正确页面
推送 前台/后台/杀进程/权限关闭/点击 到达、跳转、角标一致
存储 清缓存、清数据、卸载重装 凭证与缓存分离,无串号
性能 冷启动 ≤2s、PSS 无持续上涨、流量达标 有基线数据对比
机型 低端机、折叠屏、刘海屏、大字号 无遮挡、无截断
合规 隐私弹窗时机、权限最小化、SDK 清单 拒绝后无采集
稳定性 Monkey 5000 事件、崩溃可符号化 无新增 crash/ANR

相关阅读