每次版本提测前,我都会拉出那只猴子来跑一晚上。对做移动端测试的朋友来说,Monkey测试几乎算是app稳定性验证的入门标配。你不需要写一条测试用例,不用搭复杂的测试框架,一条adb命令就能让它像发疯了一样在屏幕上乱点,专门替你找那些人工测试根本碰不到的偶现崩溃和卡死问题。
这篇文章把我这些年跑Monkey的完整经验整理出来,从工具原理、参数设计到日志分析、问题排查一次讲清楚,适合刚接触app自动化测试的新人,也能给正在搭稳定性回归方案的老手一些参考。我会结合真实执行过程中的踩坑记录,把那些文档里不会明说的细节一并交代。
1. Monkey测试到底在测什么:从“一只猴子”说起
1.1 随机操作背后的逻辑
Monkey是Android SDK自带的一个命令行工具,名字起得很直白:它就像一个喝多了的猴子,在手机上随机乱点、乱滑、乱按。工具会在你的app上生成海量的伪随机事件流,包括点击、滑动、轨迹球、系统按键、应用切换等等,目的就是用高强度、无规律的操作把app逼到极限,看它会不会崩溃、卡死或者内存溢出。
很多人第一次接触它会觉得这工具“太傻太暴力”,完全不懂业务逻辑,连退出按钮和删除按钮有什么区别都分不清。但恰恰是这个“傻”,让它在稳定性测试里不可替代。因为真实用户的行为本身就充满随机性,你永远想不到某个用户会在什么页面连点五下返回键,又在某个空列表疯狂下拉刷新。而Monkey通过大量事件注入,模拟的就是这种极端情况下的用户行为。
核心逻辑其实很简单:事件生成器会按照你指定的比例和种子值,随机产生一系列触摸、滑动和按键事件,并把它们依次发送给被测应用。工具本身不关心应用的正确性,不校验界面跳转是否符合预期,它只负责制造压力和记录执行结果。如果程序在这个过程中挂了,那就算测试未通过。
1.2 Monkey测试和常规自动化用例的差别
这一点我要特别拎出来说,因为很多人会把Monkey测试和UI自动化混为一谈。我自己带过的测试组里,几乎每个新手第一次接到Monkey任务都会问:“是不是要写脚本?”答案是:不用。
常规的app自动化测试,比如用Appium、UIAutomator写用例,本质上是“沿着预设路径验证功能”。你打开首页,输入账号密码,点击登录,断言是否进入主界面。这套逻辑适合做功能回归,效率高、问题定位快,但它有个致命缺陷:一切都在预期内。测试用例写得再全,也覆盖不到开发自己都没想到的边界情况。
而Monkey测试是反过来的。它没有预期路径,没有业务规则,甚至没有一个断言。它的目标非常纯粹:让被测app在随机、高强度操作下存活下来。一个正常的业务页面被连续切换、快速点击、中途杀进程,还能不能保持不崩溃?在低内存状态下频繁跳转,会不会OOM?这个问题的答案,用常规自动化用例很难量化,但Monkey可以在一晚上给出结果。
所以我现在管项目里的稳定性测试,一直是“两条腿走路”:常规自动化用例负责功能正确性,Monkey测试负责“乱拳打死老师傅”式的抗压验证。两者互补,缺一不可。
2. Monkey命令实战:先把猴子放出来
2.1 从一条基础命令说起
Monkey工具不需要单独安装,它内置于Android系统镜像中。只要手机连上电脑,开启USB调试模式,就能直接通过adb执行命令。我们先用一条最简单的命令跑一次:
adb shell monkey -p com.example.app 1000这条命令的意思很直白:在指定的app(com.example.app)内随机执行1000个事件。如果你没有指定包名,Monkey会在整个系统范围内随机操作,那就会把手机里的所有应用都折腾一遍,甚至可能触碰到系统设置。所以实际测试中,我几乎一定带-p参数,把测试范围锁死在被测应用上。
执行完以后,终端会滑动输出一系列日志,以Events injected: 1000和Monkey finished结尾,说明这轮测试通过了。如果你看到** Monkey aborted due to error.,那就说明测试过程中发现了崩溃、ANR或者权限异常,后面我会专门讲怎么看这些日志。
这里有一个小细节:Monkey执行期间不需要你盯着手机,你完全可以让它自己跑着,过一段时间回来收结果。这也是它适合做稳定性回归的重要原因之一。
2.2 控制这只猴子:核心参数详解
基础命令能跑,但只能算“入门”。说到这儿,我得坦白一个经验:如果你只懂得跑裸命令,那这只猴子就完全不可控。默认情况下,Monkey生成的触摸、滑动、系统按键比例是它自己定的,可能你只想测业务页面内的点击,结果它时不时给你按一下Home键,把app切到后台去了。测试结果自然就失真了。
所以我平时使用Monkey,核心思路就是通过各种参数给它“立规矩”。下面这几组参数是我每次执行时都会重点关注的:
| 参数 | 作用 | 我的常用配置 |
|---|---|---|
-p | 指定被测应用包名,可多个 | 被测app包名 |
-s | 设置种子值,复现同一事件序列 | 固定一个整数,如-s 20241201 |
--throttle | 每个事件之间的延迟间隔(毫秒) | 300~500 |
--pct-touch | 触摸事件(点击)百分比 | 30~40 |
--pct-motion | 滑动事件百分比 | 20~30 |
--pct-majornav | 主要导航事件(如Back)百分比 | 10~15 |
--pct-appswitch | 切换应用事件百分比 | 5~10 |
--pct-syskeys | 系统按键(Home、音量键等) | 0~5 |
--pct-anyevent | 其他类型事件 | 0~10 |
-v | 日志级别,-v -v -v最详细 | 至少-v -v |
一个完整的命令看起来长这样:
adb shell monkey -p com.example.app \ --throttle 300 \ -s 20241201 \ --pct-touch 35 \ --pct-motion 25 \ --pct-majornav 15 \ --pct-appswitch 10 \ --pct-syskeys 5 \ --pct-anyevent 10 \ -v -v \ 10000这里每一个参数背后都有逻辑。--throttle控制在两个事件之间停多久,如果设置成0,Monkey会像机关枪一样疯狂输出事件,很容易把低端机的CPU打满,产生一些和“手势过快”相关的错乱;但真实用户的操作速度也没那么快,所以给一个300到500毫秒的缓冲更贴近实际场景。事件比例同理,--pct-touch调高就是让猴子集中在点击操作上,模拟用户主要用点按完成业务路径;--pct-appswitch调成10%,是为了模拟用户在app和其他应用之间来回切换的场景,这类操作对内存回收压力特别大,容易暴露内存泄漏问题。
2.3 种子值:让bug可以复现
在所有参数里,我认为最容易被忽略但最有价值的是-s种子值。
大家要理解一个关键点:Monkey生成的事件序列是伪随机,不是真随机。所谓伪随机,就是它虽然看起来乱,但本质上是由一个初始种子值按照固定算法推算出来的。换句话说,只要种子值相同、事件数相同、被测应用版本相同,Monkey就会生成一模一样的事件序列,连点哪个坐标都一致。
这个特性在定位bug时简直是救命稻草。比如昨晚Monkey跑崩了,日志里显示崩溃发生在第46320个事件。你想让开发帮忙看崩溃原因,开发肯定会问:“你能复现吗?”如果你没有用种子值,那真的很难复现,只能凭运气再乱跑一次。但你如果执行时带了-s 20241201,那很简单:
adb shell monkey -p com.example.app -s 20241201 --throttle 300 -v -v 100000 > monkey.log再跑一次,它会在同一个事件点用同样的操作序列触发崩溃。开发只需要在这条命令上配合断点调试,就能当场抓到现场。
有一回我遇到一个概率极低的崩溃,只在特定页面连续多次滑动后偶现,人工复现了三天都没成功。后来Monkey一晚上跑出来,靠的就是种子值复现定位。所以我建议所有Monkey测试都固定种子值,并且把种子值记录到测试报告里,哪怕这轮测试全部通过也别删,以后想复现任何问题都能直接找它。
3. 从日志里捞出崩溃现场
3.1 Monkey日志能告诉我们什么
很多新手跑完Monkey,看到终端滚了一大堆日志就懵了,不知道该看哪里。其实Monkey的日志虽然多,但结构非常清晰。
正常情况下,日志会包含这样几个阶段:执行开始时,输出本次测试的配置信息,包括包名、种子值、事件数和各事件比例;执行过程中,根据-v的级别输出当前注入的事件类型和事件序号;执行结束时,输出统计信息,包括总事件数、注入的事件类型分布以及Monkey finished标记。
我习惯用重定向把日志完整保存下来:
adb shell monkey -p com.example.app -s 20241201 --throttle 300 -v -v 10000 > monkey_test.log 2>&1保存以后,先在文件里搜几个关键标记:
- 搜“Monkey finished”确认全程正常结束。
- 搜“CRASH”定位崩溃导致的终止。
- 搜“ANR”定位无响应问题。
- 搜“aborted due to error”定位异常中断。
日志级别这块,-v越多信息越详细。平时回归我一般用-v -v,能看到每个事件的类型和序号,又不至于让日志文件膨胀到几百MB。如果需要精确定位崩溃前最后一个操作是什么,我会用-v -v -v执行一个短时间定位测试,信息量足够,日志大小也能接受。
3.2 读崩溃日志的实战套路
如果Monkey在运行过程中发现app崩溃,日志末尾通常会出现一段这样的内容:
// CRASH: com.example.app (pid 12345) Short Msg: java.lang.NullPointerException Long Msg: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.os.Bundle.getInt(java.lang.String)' on a null object reference Build Label: xxx这段信息里的Short Msg和Long Msg已经能直接告诉我们是哪类异常、在哪个方法调到了空对象。但注意,Monkey自身日志里的堆栈信息通常很短,要拿到完整堆栈,还得配合抓取logcat。因为Monkey崩溃发生时,Java层异常堆栈同样会输出到logcat,而且带详细的行号方法名。
我的标准操作流程是这样的:先让Monkey正常跑着,同时在电脑上开一个终端持续抓取AndroidRuntime日志:
adb logcat -s AndroidRuntime:E > crash_error.log等Monkey结束,打开crash_error.log,搜“FATAL EXCEPTION”,就能看到完整的异常线程堆栈。再顺着堆栈找自己业务代码的行号,把崩溃点定位到具体函数。
如果出现的是ANR(Application Not Responding),日志里一般会有ANR in com.example.app的标记,而且Monkey会因为超时中断。这种情况建议在崩溃前后各抓一次logcat,重点看main线程在等待什么操作。如果测试机有root权限,可以直接拉取/data/anr/下的trace文件分析;没root的机器,可以试试adb bugreport方式收集。
排查时间长了你会发现,Monkey日志的最大价值不是那个崩溃标记本身,而是它附近记录的最后一批事件,那些事件序列就是崩溃的触发路径。
4. 正经的Monkey测试方案长什么样
4.1 事件数和参数比例怎么定
接Monkey这个任务,新人最爱问的一句话是:“到底要跑多少次事件才算完?”我的回答是:先看你的测试目标和时间窗口。
如果是冒烟回归,目标只是验证本轮改动没有特别严重的稳定性问题,跑5000到10000个事件就够了,大概十几分钟出结果。这个档位适合每个迭代的提测阶段,快速圈定问题范围。
如果是常规稳定性测试,我一般跑到50000个事件左右,耗时约两三个小时,基本能覆盖大部分异常路径。如果项目版本临近发布,或者这个模块之前有过崩溃前科,我会直接拉一个过夜的稳定性测试,跑20万到50万事件。这里有一个需要特别注意的地方:事件数和--throttle共同决定总时长。假设你设置--throttle 300,每秒钟约注入3个事件,50万事件大约需要46个小时,这种强度通常周末或晚间跑比较合适。
参数比例方面,我常用的套路是根据业务类型做微调:
| 应用类型 | touch | motion | majornav | appswitch | syskeys | anyevent |
|---|---|---|---|---|---|---|
| 电商购物类 | 35 | 25 | 15 | 10 | 5 | 10 |
| 工具效率类 | 25 | 20 | 25 | 15 | 5 | 10 |
| 视频影音类 | 30 | 35 | 10 | 10 | 5 | 10 |
| 游戏类 | 45 | 30 | 10 | 5 | 5 | 5 |
这套比例不是官方标准,是我根据业务特点迭代出来的。电商类重点是商品列表和交易页面,点击和滑动节奏要接近真实用户;游戏类操作高频且集中在点击和滑动上,系统导航反而会打断沉浸式体验,所以比例设得很低。设置完之后,别忘了一个细节:检查所有比例加起来是否接近100%。如果加起来和100差太多,Monkey会随机分配剩余比例,这时候整个测试的事件分布就不可控了。
4.2 执行前环境准备和执行策略
Monkey测试虽然不用写用例,但执行前的环境准备一样不能马虎。我在团队里反复强调过一句话:测试结果失真,往往不是猴子的问题,而是环境的问题。
第一,保持屏幕常亮。默认情况下手机息屏后,Monkey会停止注入事件,导致测试白跑。执行前务必执行:
adb shell svc power stayon true测试结束后再恢复:
adb shell svc power stayon false第二,预先授予运行时权限。现在的app普遍有大量动态权限请求,比如定位、相机、存储。如果权限弹窗在Monkey执行过程中弹出来,猴子会无脑乱点,有可能点到“拒绝”,后边的业务流程直接走不下去。测试结果要么变成“没有崩溃但其实也什么都没测到”,要么因为弹窗拦截导致大量事件没有真正作用于业务页面。我习惯在跑Monkey前就把关键权限全部授权:
adb shell pm grant com.example.app android.permission.CAMERA adb shell pm grant com.example.app android.permission.ACCESS_FINE_LOCATION第三,处理好登录态。很多app不登录就只能停留在引导页或者登录页,Monkey再怎么点也进不了核心业务。所以我一般会先手动登录好测试账号,再启动Monkey。这样猴子能真正深入到下单、支付、消息中心这些核心模块里。
第四,就是多包场景。如果你们产品和多个app有联动,也可以在一轮测试里指定多个包:
adb shell monkey -p com.example.app -p com.example.pay -p com.example.push -v 50000防止Monkey乱跳到其他应用时,因为目标应用不在测试范围里而意外中断。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
实话说,Monkey测试本身不复杂,复杂的是测试过程中遇到的那些“看起来跟Monkey无关但又直接影响结果”的问题。我把这些年高频遇到的问题整理成一个速查表,照着处理基本不会跑偏。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| Monkey跑着跑着停住,返回桌面 | 系统按键比例过高,误触Home键 | 调低--pct-syskeys,必要时设为0 |
| 日志显示“No activities found to run” | 包名写错,或app未安装 | 用adb shell pm list packages确认包名 |
| 权限弹窗频繁出现,测试失真 | 运行时权限未预授权 | 执行前用pm grant批量授权 |
| 测试过程中屏幕自动熄灭 | 系统休眠策略 | 设置svc power stayon true |
| 日志尾部出现“** Monkey aborted due to error.”但没有Java堆栈 | 可能触发了系统保护或native崩溃 | 抓logcat看DEBUG和libc输出 |
复测时用相同-s还是复现不了 | 事件数不一致,或app版本有差异 | 确认种子值、事件数、版本号三个条件一致 |
| 国产ROM跑Monkey被系统自动清理 | 系统后台限制或省电策略 | 锁定测试机后台,在设置里关闭该app的省电优化 |
有一条是很多测试新人最容易踩的坑:Monkey在测试过程中因为某些异常事件导致系统UI进程(比如SystemUI)无响应,整个手机看起来像死机了。这种情况不是被测app的崩溃,而是系统层面的问题。处理方法是先区分是系统进程还是应用进程,如果测试机本身低端且内存不足,建议换一台性能余量更大的机器,否则测试结果里会混入太多和业务无关的干扰项。
5.2 我的几个独家心得
最后分享几个在多次实战中总结的经验,这几个点官方文档里基本找不到,但对跑好Monkey测试挺关键。
第一个心得:不要把Monkey跑过一次、通过了就丢到一边。我的习惯是每个版本都跑Monkey,并且把种子值、事件数、测试时间、机器型号全部记录成一个固定的测试基线。等到下个版本再跑同样配置的Monkey,横向对比崩溃率和ANR次数。虽然Monkey是随机事件,但固定种子值和事件数后,不同版本之间的事件序列是一样的,这个对比就有意义。哪个版本稳定性出现劣化,一眼就能看出来。
第二个心得:高版本Android上遇到存储相关崩溃要格外重视。Android 10之后的存储权限策略收紧,很多老代码在读写文件时会悄悄崩溃。这种bug平时手工测试很难触发,反而被Monkey的随机事件频繁撞上。遇到这种问题不要觉得是猴子“点歪了”导致的无效崩溃,它恰恰是真实用户同样会遇到的高频问题。
第三个心得:Monkey跑完之后,别忘了清理现场。测试过程中调节音量、切换WiFi、打开蓝牙都是家常便饭,我见过程序跑完整个手机音量被调到最大,还自动连上了其他蓝牙设备。恢复现场不是洁癖,是为了不影响下一个测试任务的初始环境。
第四个心得我想特别说给测试新人:不要迷信Monkey的“未崩溃”结果。Monkey的长处是发现偶现崩溃,而不是验证功能正确性。即使Monkey十万事件全数通过,也说明不了业务逻辑没有错。它是一道“保底”的检查线,但绝不是唯一的质量防线。
最后再分享一个实用技巧:如果你要跑的Monkey事件数很大,建议分多个日志文件分段保存,别挤在一个文件里。用后台执行和按时间切片保存的方式,比如每小时一个log文件,这样一旦某个时段出了问题,我们能快速定位是事件区间的哪一段,排查起偶现问题会顺手很多。