1. 从“手动点断手”到“脚本跑断腿”:聊聊小程序UI自动化的真实起点
做微信小程序测试的人,应该都经历过那种“点来点去”的日子。小程序页面状态多、弹窗乱入、登录态一失效就要重新扫码,测试回归一遍下来,少说也得半小时。尤其是做电商、社区、工具类小程序,改一个组件或者调一个接口,可能就把之前的核心流程带崩了。我前前后后带过好几个小程序项目,踩了无数坑之后,终于把UI自动化这套东西跑通并沉淀了下来。这篇就是给那些正准备入坑小程序UI自动化的人看的,也顺带聊聊我在实践中踩过的那些经典“坑”和最终稳定运行的方案。
很多人以为小程序UI自动化很难搞,其实现在的技术工具链已经相当成熟了。关键在于选对方案,并且理解微信小程序自身的运行机制。小程序不像普通H5那样,你在浏览器里拿到一个URL就能用Selenium开搞。它的页面结构是基于自研的渲染引擎,有双线程模型,跟常规的Web DOM并不完全一致。所以,我们得先搞清楚,小程序UI自动化的核心链路是什么,再谈怎么写用例。
我实测下来,目前主流的做法可以分成三条路线:一是微信官方提供的miniprogram-automator,二是基于Appium+minicap等方式去做真机层面的驱动,三是部分第三方云测平台自己封装的SDK。三条路线各有优劣,需要按项目阶段去做取舍。这篇文章会重点讲透我常用的“官方SDK+本地开发者工具”的落地方式,这个方案对大多数中小团队来说,是成本最低、见效最快、也最容易维护的。
在动手之前,我想先给你打个预防针:UI自动化测的从来不是“业务逻辑”,而是“页面表现流程”。如果你的项目页面结构不稳定、组件乱写、没有良好的语义化命名,那不管用什么框架,用例都会写得极其痛苦。所以,做UI自动化,本质上是反推研发团队去规范页面结构,间接提升代码质量,这个价值甚至比“发现BUG”本身更重要。
2. 方案选型:为什么我推荐miniprogram-automator而不是Appium
2.1 迷你版官方驱动,到底解决了什么问题
miniprogram-automator是微信官方在开发者工具基础上做出来的一套Node.js SDK。它对外提供了一套类似于操作浏览器的API,我们可以用它启动微信开发者工具、打开指定的小程序项目、控制页面跳转、获取页面数据、触发点击事件,然后做断言。它的本质是“通过开发者工具的自动化接口去驱动小程序”。
跟Appium这种通用移动端自动化框架比,官方驱动有个先天优势:它走的是微信开发者工具内置的调试通道,不需要你处理各种设备连接问题,也不需要你关心UIAutomator或者XCTest底层差异。而且它可以直接拿到小程序的Page实例、组件数据,这对断言内部状态来说非常友好——直接比对data字段比在页面上扒文案方便多了。
我最早其实是在Appium上折腾的,那时候小程序还没有跨平台成熟方案,得用webview切换的奇技淫巧,稳定性一言难尽。后来微信官方出了miniprogram-automator,果断迁过来了,测试脚本量大概是原来的三分之二,维护成本更是直线下降。
2.2 三条技术路线的对比,谁更适合你
这里我直接放一张我整理的对比表,方便你做初步决策:
| 方案 | 运行环境 | 稳定性 | 上手成本 | 适用场景 |
|---|---|---|---|---|
| miniprogram-automator | 微信开发者工具 | 较高 | 低 | 开发自测、常规UI回归、CI环境 |
| Appium + webview调试 | 真机/模拟器 | 一般 | 高 | 需要覆盖真机设备特性的场景 |
| 云测平台SDK | 云真机 | 取决于供应商 | 中 | 大规模兼容性回归、远程设备矩阵 |
如果你只是想把现有小程序的“核心功能冒烟测试”和“常规回归测试”跑起来,那最优先选官方驱动。而如果你要追的是“微信版本升级兼容性”“不同安卓机型上的页面渲染”,那Appium体系还是有存在的必要,只是成本也高得多。我自己团队的做法是:本地、CI统一跑官方驱动,定期再抽一批真机交给云测平台去做兼容性回归。
2.3 为什么把开发者工具“装进流水线”是关键
默认情况下,微信开发者工具就是个IDE,你需要手动打开、手动编译、手动操作。而UI自动化要跑起来,第一步就是让工具能被“命令行控制”,也就是把开发者工具当成一个可编程的运行时环境来使用。
微信开发者工具本身提供了一个命令行工具cli,可以通过类似cli auto --project 项目路径 --auto-port 9420的方式启动自动化端口。自动化测试脚本再通过这个端口连接进入项目环境。需要注意的是,只有微信开发者工具登录了微信账号,且打开了“服务端口”开关,外部脚本才能连上。这一点是新手踩坑最多的地方。
具体的选择逻辑其实很简单:哪个工具能稳定地被脚本控制、能稳定的保活、能在无人值守环境下重启,哪个就值得作为自动化的承载平台。桌面版微信开发者工具虽然需要一个GUI环境,但在Linux CI机器上也可以通过一些显示虚拟化方案跑起来(比如Xvfb),这个后面我会展开讲。
3. 环境搭建:把工具、项目和SDK三者串起来
3.1 从零初始化一份可以跑通的自动化工程
我一般会用一个独立的Node项目来维护所有UI自动化用例,然后通过npm script去控制执行。这个项目只做UI自动化相关的事情,和被测小程序项目本身分开。
第一步,需要安装miniprogram-automator作为依赖,用npm或者yarn都行。
npm init -y npm install miniprogram-automator --save-dev第二步,需要确认微信开发者工具的版本。用官方SDK时,我强烈建议你用最新稳定版工具,因为有些自动化接口和编译能力会随开发者工具更新而变化。项目里有个project.config.json,要确保其中的appid是你自己在公众平台申请的真实AppID或者测试号。
第三步,手动打开一次微信开发者工具,并导入小程序项目。这一步是为了让工具记住项目,并且允许后续命令行自动打开。同时确保在“设置 -> 安全设置”里,开启了“服务端口”。如果没开启,后面连接时会直接报错,提示你无法连接自动化端口。
3.2 启动/连接开发者工具的自动化通道
现在要跑一个最小化冒烟脚本。先确认路径,开发者工具CLI在macOS上通常位于/Applications/wechatwebdevtools.app/Contents/MacOS/cli,在Windows上则类似C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat。
可以先手动执行一下自动化启动命令:
cli auto --project /path/to/your/miniprogram --auto-port 9420这个命令会打开项目并监听9420端口。接着,在Node脚本里通过miniprogram-automator连接:
const automator = require('miniprogram-automator'); async function run() { const miniProgram = await automator.connect({ wsEndpoint: 'ws://localhost:9420', }); // 拉取当前页面栈 const page = await miniProgram.reLaunch('/pages/index/index'); await page.waitFor(500); // 做点什么 const title = await page.$('.page-title'); console.log(await title.text()); await miniProgram.close(); } run();跑起来后,如果能在控制台看到页面里某个元素的文本内容,说明环境串起来了。这一步能通,后续就等于打通了任督二脉,下面的用例编写、数据层断言、组件交互,全都可以在这个通道上做。
提示:
connect时,如果提示连接失败,优先检查:1. 开发者工具是否打开了自动化端口;2. 项目是否已经导入;3. 端口号是否一致。这几步占了环境问题中的八成。
3.3 把自动化工具跑在Linux CI服务器上
如果你们团队已经把自动化用例放到CI里跑,那大概率会遇到一个问题:Linux服务器上没有桌面环境,微信开发者工具是GUI程序,怎么让它跑起来?
我实验出来的稳定方案是安装Xvfb,用虚拟显示去承载开发者工具。在Ubuntu上可以执行:
sudo apt-get install -y xvfb Xvfb :99 -screen 0 1280x800x24 & export DISPLAY=:99然后再执行开发者工具的启动命令。实测下来,稳定性还不错。只是一定要在CI配置里预留足够的启动时间,别一启动就立刻执行connect,最好增加重试机制,因为开发者工具冷启动有时会需要好几秒。
另外,建议在CI里把开发者工具安装为独立版本,和本地日常开发使用的版本隔离。否则每次CI强制更新或者日常手动升级工具,都可能导致自动化运行环境被意外改变,非常难受。
4. 核心实操:页面跳转、元素定位和断言到底怎么写
4.1 页面对象(Page)和数据生命周期的理解
小程序页面和传统Web页面有个本质区别:小程序的逻辑层和渲染层是分开的。UI自动化在驱动页面的时候,能看到的数据其实是从逻辑层同步过去的,因此拿到element之后执行点击、输入等操作,最终触发的是框架里的setData等动作。
用官方SDK操作页面时,通常先拿Page对象,再拿元素:
// 直接通过 reLaunch 打开页面 await miniProgram.reLaunch('/pages/goods/list'); const page = await miniProgram.currentPage(); // 通过 .$ 找到元素 const goodsItem = await page.$('.goods-item'); await goodsItem.tap();这里有个细节,currentPage()拿到的是当前显示的页面对象,如果你调用reLaunch之后立刻调用currentPage(),有可能因为页面切换动画还没结束导致拿不到正确的页面。可以考虑加一个等待,或者使用waitFor的方式等待页面关键元素出现。
另外,小程序的页面也是一层一层挂载的,navigateTo可以叠加栈,reLaunch会清空栈。编写用例时,如果你只是想在当前栈基础上跳到一个新页面做测试,可以使用navigateTo,但要注意用例跑完后要把页面栈跳回来或者重新启动,避免对后续用例造成干扰。
4.2 定位元素的方式:选择器、文本、自定义属性
小程序官方驱动支持通过CSS选择器来定位元素,这个能力是真的香。我们可以用.class、#id、[data-testid="xxx"]、甚至::text("文案")这类伪选择器。看官方文档时你可能见过一些$方法,但我强烈建议在团队里统一约定“自动化测试专用定位属性”,比如>const page = await miniProgram.currentPage(); const loginBtn = await page.$('[data-testid="login-btn"]'); await loginBtn.tap(); // 也可以在元素上做文本判断 const userName = await page.$('.user-name'); console.log(await userName.text());
如果实在没有合适属性,也可以使用文本选择器。文本选择器的写法比较特殊,比如page.$('view[data-testid="xxx"]')会用CSS路径,而page.$$('text=首页')这种写法也能在某些版本中生效,但稳定性一般,所以只建议用作文本断言的辅助方式。
4.3 弹窗、组件、原生控件的“不可控”问题
小程序里一旦涉及到原生组件(像某些输入框、地图、视频、canvas、textarea),UI自动化的可操作性会肉眼可见地下降。这个问题不仅存在于官方SDK,也存在于其他方案中。原生组件在很多版本里是显示在webview之上的,普通元素选择器根本捕捉不到。
我的经验是:优先在业务侧把这类控件替换成可测试的封装组件,或者给原生组件包一层容器,暴露操作按钮等替代交互。比如地图选点、日期时间选择等,都可以通过数据注入或者隐藏按钮的方式去绕过原生控件。
另外,很多小程序项目用了第三方组件库,比如Vant Weapp、TDesign等。这些组件内部往往渲染多层结构,不能直接用文字断言。不过组件是自带行为事件的,可以配合“页面数据”的变化来验证组件交互是否生效。这也回应了前面那个观点:小程序UI自动化的核心,不一定全在UI上,很多数据响应逻辑通过page.data()去断言更靠谱。
const page = await miniProgram.currentPage(); const initialData = await page.data('count'); // 点击加号按钮 await (await page.$('[data-testid="increase-btn"]')).tap(); await page.waitFor(200); const newData = await page.data('count'); // 断言 if (newData !== initialData + 1) { throw new Error('点击加号后count未变化'); }这里验证的是数据层状态,比单纯看页面文案稳定得多,尤其适合表单、列表筛选、计数类交互。
5. 进阶实战:把UI自动化跑进日常CI流程
5.1 在Jenkins/GitLab CI中接入完整回归脚本
很多团队一开始只在本地手动跑一下UI自动化脚本,但真正的价值点在于接入CI,让每次提测、每次发布前都自动跑一遍核心回归。
我在Jenkins上做过一个流水线,大概的步骤是:
- 从Git拉取被测小程序代码。
- 使用Node脚本执行依赖安装。
- 初始化Xvfb(Linux环境)。
- 启动微信开发者工具的自动化模式。
- 运行测试用例(用的Jest runner)。
- 生成测试报告并归档。
- 关闭开发者工具,清理进程。
下面这段是Jest runner里测试文件的简单例子:
const automator = require('miniprogram-automator'); describe('小程序购物车流程', () => { let miniProgram; beforeAll(async () => { miniProgram = await automator.launch({ projectPath: process.env.MP_PROJECT_PATH, port: 9420, }); await miniProgram.reLaunch('/pages/home/home'); }); afterAll(async () => { await miniProgram.close(); }); it('添加购物车后角标数量变化', async () => { const page = await miniProgram.currentPage(); await (await page.$('[data-testid="add-cart-btn"]')).tap(); await page.waitFor(500); const badgeText = await (await page.$('.cart-badge')).text(); expect(badgeText).toContain('1'); }); });这里有个经验:如果启动开发者工具的过程比较慢,导致Jest在beforeAll里连接失败,可以在外面封装一个启动等待函数,循环等待端口可用。
5.2 用例分组与冒烟/全量回归策略
UI自动化最忌讳的就是“一个失败,全盘崩”,所以要建立用例分组的习惯。我从实践中总结的分组策略是:
| 分组 | 范围 | 执行时机 |
|---|---|---|
| smoke | 登录态、首页加载、核心路径跳转 | 每次代码提交后的快速检查 |
| regression | 所有核心业务用例 | 每日夜间定时跑 |
| page-detail | 针对某个页面的专项测试 | 页面改动频繁或上线前特批时执行 |
在Jest里可以用testPathPatterns来匹配分组的文件,比如:
npx jest --testPathPatterns="tests/smoke"这样,日常提交只跑冒烟,夜间才跑全回归,兼顾效率和稳定性。
5.3 与小程序“体验版”和“开发版”发布流程的配合
这里多说一句,热词里很多人会问“体验版二维码在哪”“线上小程序怎么拿源码”。从测试的角度讲,UI自动化建议统一跑“开发版”或当前本地构建出来的代码包,不要去跑线上“正式版”。原因是线上环境和本地代码包不一定同步,也无法随意注入环境变量或者开启调试模式。
我们团队的做法是,在CI构建出小程序包后,先把构建产物自动导入开发者工具,跑完自动化用例,再走提审或上传体验版流程。这样能保证测试的就是即将发布的那个包,把“测试包和发布包不一致”的坑提前避掉。
6. 常见问题与排查技巧实录
6.1 开发者工具连不上,端口不通
这基本是新手第一个遇到的墙。现象是connect调用后长时间未连接或直接报Error: connect ECONNREFUSED。
排查思路:
- 检查开发者工具的“设置 -> 安全设置 -> 服务端口”是否开启。
- 检查命令行启动参数里的端口是否和
connect里的端口一致。 - 检查是否有多个开发者工具实例占用同一端口,建议把其他实例先关掉。
- 在CI环境,注意跨容器访问时IP不能写成
localhost,要按实际网络配置来写。
多数情况下,以上四步就能解决八成连不上问题。
6.2 运行一段时间后,页面元素定位失败
这种情况比较诡异,用例前面执行正常,跑到十几分钟后开始大面积失败。经验是开发者工具长时间运行会占用大量内存,或者页面栈/缓存越来越多导致样式错乱。
我的办法是:跑完每组用例后主动重启开发者工具,或者给整个自动化任务设置定时重启。在CI上我直接封装了“执行前自动kill已存在的开发者工具进程,再重新拉起来”的逻辑。最终效果很稳。
6.3 点击元素偶发失效,tap没有反应
这通常不是代码问题,而是操作太快了。小程序的渲染和逻辑之间有个异步链路,数据更新后组件不一定马上可点击。我总结的稳定写法是“三步走”:
async function stableTap(page, selector) { const element = await page.$(selector); await element.waitFor(); await page.waitFor(300); await element.tap(); await page.waitFor(300); }虽然看起来有点傻,但这类固定小延时能很大程度提升点击成功率。如果你不想写死延时,也可以改成循环尝试:如果点击之后页面没有预期变化,就再点一次,直到超过重试次数。
6.4 页面里存在多个相同>const cards = await page.$$('[data-testid="goods-card"]'); if (cards.length < 2) { throw new Error('商品列表数量不足'); } const secondCard = cards[1]; await secondCard.tap();
需要注意,$$返回的元素是数组,你在运行时把它当成普通元素用会报错,建议多打印一下数组长度,避免选择器没匹配到任何节点。
6.5 自动化脚本不稳定,偶发失败因素汇总
这里是我实际跑自动化过程中总结的“不稳定来源”清单:
| 不稳定因素 | 影响程度 | 应对方式 |
|---|---|---|
| 开发者工具版本升级 | 高 | 锁定自动化专用工具版本 |
| 网络请求耗时波动 | 中 | 对接口进行mock或增加等待策略 |
| 页面渲染动画时长 | 中 | 统一等待关键元素出现 |
| 测试数据残留 | 高 | 执行前重置用户数据和后端状态 |
| 组件库内部行为差异 | 中 | 尽量用数据断言替代纯UI断言 |
| 用例执行顺序影响 | 高 | 每个用例独立启动或清理登录态 |
这份清单基本是每个做小程序UI自动化的人都绕不开的坑。提前知道,能帮你避免很多“玄学问题”。
7. 关于“小程序签名/抓包/多端适配”等延伸问题的思考
在热词里总能看到“微信小程序签名”“微信小程序抓包”这类疑问。签名和抓包虽然不直接属于UI自动化范畴,但它们往往会在实践中冒出来。比如你要调试自动化脚本里某个请求的返回,可能需要临时抓包;你要校验某些登录态的签名逻辑,也会对自动化用例构造数据造成阻碍。
我可以分享的经验是:小程序UI自动化的数据准备阶段,我们通常不会依赖前端页面走真实登录流程,而是通过后端接口直接造token、再注入到小程序缓存中。但小程序内部的请求可能会有签名校验,这类校验一般需要研发侧提供一个“测试签名模式”,或者提供一份可复用的签名生成工具库。别指望UI自动化去破解签名,那是没意义的。
另外,如果你在开发“小程序a跳转小程序b”的流程,UI自动化能不能覆盖?可以覆盖一部分。比如点击跳转按钮后校验跳转动作是否被触发,但真要验证目标小程序的落地页,还是要进入目标小程序去继续跑另外一套自动化工程。这个边界要提前想清楚,别在一个工程里塞太多跨小程序场景,否则维护成本会成倍上升。
还有一点容易被忽略:真机上小程序顶部导航栏高度、安全区适配和开发者工具里看到的并不完全一样。虽然UI自动化主要跑在开发者工具里,但如果有“导航栏自定义”这类功能,建议额外在真机上抽测一遍,因为自动化脚本只能保证逻辑链路没问题,没法完整代替真机渲染验证。
8. 绕不开的性能与稳定性:跑UI自动化也要把“资源账”算清楚
UI自动化看着只是“点一点”,实际上消耗的资源一点都不少。开发者工具本身就吃内存,一个复杂小程序项目同时打开几个页面后,电脑风扇疯狂转是常态。如果你们团队只有一台共享测试机,要安排好多套用例的并行节奏,别在同一时间搞十几条任务,否则会把机器拖死。
我这边把UI自动化分成了两档:日常快速回归,控制在2到3分钟内跑完冒烟用例;夜间全量回归,控制在20分钟左右。超出这个时间范围后,用例产出效率和稳定性都会下降。一个任务超过30分钟,我建议就开始考虑是否需要拆分成多个任务或减少用例深度了。
另外,测试报告也值得关注。很多人跑完UI自动化只看“通过了没”,但真正有价值的其实是失败截图、页面调用日志、控制台报错。miniprogram-automator里可以用page.screenshot()截图,遇到断言失败时自动保存截图并attach到报告里,能省去一大截排查问题的时间。
async function screenshotOnFail(page, testName) { const screenshot = await page.screenshot(); require('fs').writeFileSync(`./reports/${testName}.png`, screenshot); }把这段逻辑集成到Jest的afterEach里,只要用例失败就自动截图,定位问题的效率会直线上升。
9. 最后一个经验:把UI自动化当“产品”来运营,而不是一次性脚本
很多人搭好框架、写完用例、跑通一版后就万事大吉了,但过了一两个月,用例开始大面积失效,然后又觉得自动化是负担。这里我想说,一个能长期运转的小程序UI自动化体系,必须有持续的运维投入。
我目前的做法是每周固定抽一小时做“用例健康度巡检”。巡检内容包括:哪些用例最近连续失败、哪些页面元素被改了导致选择器失效、哪些业务流程发生了调整、哪些用例可以合并或移除。同时,我会让研发在代码评审时同步关注是否影响到了>