1. 项目概述:从手动到自动的跨越
作为一名长期与小程序打交道的开发者,我深知在项目迭代和回归测试中,手动点点点的痛苦。每次发布新版本,光是核心流程的走查就要耗费大半天,更别提那些需要反复验证的边界条件了。直到最近,我决定彻底改变这种低效的工作模式,将目光投向了官方提供的自动化测试工具——miniprogram-automator。这个工具本质上是一个Node.js库,它允许我们通过脚本代码,像真实用户一样去操作小程序,模拟点击、滑动、输入等行为,并获取页面状态和数据。这不仅仅是“偷懒”,更是提升开发质量、保障项目稳定性的必要手段。如果你也受够了重复的手工测试,或者想为你的小程序项目引入自动化回归能力,那么我这次从零开始的探索过程,或许能给你提供一份详实的参考地图。
2. 工具选型与环境搭建的底层逻辑
在开始动手之前,花点时间理解工具生态和搭建一个稳定的环境,远比直接敲命令更重要。市面上并非没有其他小程序自动化方案,比如一些基于图像识别的测试框架。但miniprogram-automator的核心优势在于其“原生”属性。它由微信官方团队开发,通过WebSocket协议与开发者工具直接通信,这意味着它的操作指令能最精准地映射到小程序的运行时环境,稳定性和可靠性更高。它不依赖UI截图,而是直接操作小程序底层的组件树,执行速度更快,且不受界面样式变化的影响(只要组件结构稳定)。
2.1 环境依赖的精确匹配
自动化脚本运行在Node.js环境中,因此第一步是确保Node.js版本合适。经过实测,miniprogram-automator对Node.js版本有一定要求,太老的版本(如Node.js 10)可能无法兼容其依赖的某些新特性。我推荐使用Node.js 14 LTS或16 LTS版本,这是目前生态兼容性最好的长期支持版。你可以通过终端命令node -v来检查。
接下来是安装工具包本身。这里有一个关键选择:是全局安装还是项目内安装?我强烈建议采用后者。在项目根目录下执行npm install miniprogram-automator --save-dev,将其作为开发依赖安装。这样做的好处是隔离了环境,不同项目可以使用不同版本的automator,避免了全局污染。安装完成后,你还需要确保电脑上安装了最新稳定版的微信开发者工具,并且已经登录了开发者账号。因为automator需要启动一个无界面的开发者工具实例(我们称之为“CLI”)来加载小程序项目。
2.2 项目配置的“暗坑”排查
环境就绪后,需要对你待测试的小程序项目进行简单配置。核心是确保开发者工具的设置正确。打开微信开发者工具,进入“设置 -> 安全设置”,你需要开启服务端口。这个端口是automator连接开发者工具的桥梁。你可以选择一个固定的端口号,比如9527,并记下它。
注意:很多新手在这里会忽略一个细节:如果你的电脑有多个微信开发者工具账号(例如公司账号和个人账号),务必确保你启动CLI和后续连接时,使用的是同一个登录态的端口。否则会出现连接失败。一个稳妥的做法是,在脚本中启动CLI时,明确指定开发者工具的安装路径和端口号。
3. 核心流程拆解:连接、启动与基础操作
理解了原理和配好了环境,我们就可以开始编写第一个自动化脚本了。整个过程可以清晰地分为三个步骤:启动开发者工具CLI、连接小程序、执行自动化操作。
3.1 启动CLI与建立连接
我们首先创建一个名为test.js的脚本文件。第一步是启动无界面的开发者工具。miniprogram-automator提供了launch方法来完成这个任务。这里有几个关键参数需要配置:
const automator = require('miniprogram-automator'); async function runTest() { // 步骤1: 启动CLI const miniProgram = await automator.launch({ projectPath: '/absolute/path/to/your/project', // 小程序项目的绝对路径,至关重要! cliPath: '/Applications/wechatwebdevtools.app/Contents/MacOS/cli', // 开发者工具CLI的绝对路径 // Windows示例: cliPath: 'C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat' }); // 步骤2: 连接小程序 const miniProgram = await automator.connect({ wsEndpoint: 'ws://127.0.0.1:9527', // WebSocket地址,端口需与开发者工具设置一致 }); } runTest();关键点解析:
projectPath:必须使用绝对路径。使用相对路径是导致“找不到项目”错误的最常见原因。在Node.js脚本中,可以使用path.resolve(__dirname, ‘../project’)来动态构造绝对路径。cliPath:这个路径因操作系统和安装方式而异。在Mac上,它通常隐藏在.app包内;在Windows上,它是一个独立的cli.bat文件。找不到它,CLI就无法启动。wsEndpoint:端口号9527需要替换为你自己在开发者工具中设置并开启的端口。连接前,请确保开发者工具已经打开(或通过CLI启动),并且该端口可用。
3.2 页面导航与元素定位
连接成功后,我们就获得了操作小程序的入口miniProgram对象。最常用的操作是跳转页面和获取页面对象。
// 跳转到指定页面 await miniProgram.navigateTo('/pages/index/index'); // 获取当前页面对象 const page = await miniProgram.currentPage(); // 等待页面元素渲染完成(非常重要!) await page.waitFor(500); // 简单等待500毫秒,非最优方案 // 更优方案:等待特定选择器出现 await page.waitForSelector('.submit-btn');获取页面对象后,就可以定位页面上的元素了。miniprogram-automator支持多种选择器,类似于CSS选择器,但它是基于小程序自定义组件的结构。
- .class选择器:最常用,如
.user-name。 - #id选择器:如
#login-btn。 - 自定义组件选择器:如
tagName。 - 组合选择器:如
view.container > .item。
定位到元素后,就可以模拟用户交互了:
// 定位一个按钮并点击 const btn = await page.$('.primary-button'); await btn.tap(); // 定位输入框并输入文本 const input = await page.$('.search-input'); await input.input('自动化测试'); await page.waitFor(300); // 输入后稍作等待,模拟用户停顿 // 获取元素的属性、文本或样式 const text = await btn.text(); const color = await btn.style('color'); const isDisabled = await btn.attribute('disabled');4. 复杂交互与状态捕获的实战技巧
基础的点击和输入只是开始,真实的用户场景要复杂得多。例如滑动列表、长按操作、处理弹窗、获取网络请求数据等。
4.1 处理滑动与长按
对于滚动列表,我们需要模拟滑动(swipe)操作。miniprogram-automator的page对象提供了swipe方法。
// 向下滑动屏幕(模拟下拉刷新) await page.swipe('down', 100); // 方向, 滑动距离(px) // 更精确的滑动:在某个元素区域内滑动 const scrollView = await page.$('.scroll-view'); await scrollView.swipe('up', 200); // 模拟长按操作 const deleteBtn = await page.$('.delete-item'); await deleteBtn.tap({ duration: 2000 }); // 长按2秒4.2 应对弹窗与模态框
小程序中常见的showModal,showToast等API产生的弹窗,在自动化脚本中需要特殊处理,因为它们不在页面DOM树内。我们可以通过监听页面事件来捕获。
// 监听 modal 确认事件 page.on('modalConfirm', async () => { console.log('用户点击了模态框的确认'); // 可以在这里执行确认后的操作,比如继续下一步测试 }); // 触发一个会弹出模态框的操作 await page.$('.show-modal-btn').tap(); // 如果需要点击取消,则监听 ‘modalCancel’更通用的方法是,在操作可能触发弹窗的按钮后,使用page.waitFor等待一段时间,让弹窗完全显示,然后再通过选择器去查找弹窗上的按钮。但注意,弹窗的根节点可能在页面层级之外,选择器可能需要调整。
4.3 捕获网络请求与Console日志
这对于测试接口是否正确调用、参数是否正常传递至关重要。miniprogram-automator允许我们监听小程序的网络请求和Console输出。
// 监听网络请求 miniProgram.on('request', request => { console.log('请求URL:', request.url); console.log('请求方法:', request.method); console.log('请求数据:', request.data); // 可以在这里做断言,检查请求是否符合预期 }); // 监听Console输出 miniProgram.on('console', msg => { console.log('小程序Console:', msg.type, msg.text); // 可以捕获到小程序中 console.log, console.error 的输出 });这个功能非常强大,你可以验证一个“提交订单”操作是否发出了正确的POST请求,或者当页面出错时,是否能捕获到console.error信息,从而快速定位问题。
5. 构建健壮测试脚本的工程化实践
写几个零散的操作命令不难,但要构建一套可维护、可复用的自动化测试套件,就需要一些工程化思维。
5.1 使用Async/Await处理异步
小程序自动化中的所有操作几乎都是异步的。使用async/await语法可以让代码逻辑像同步一样清晰,避免“回调地狱”。
async function testLoginFlow(miniProgram) { try { await miniProgram.navigateTo('/pages/login/login'); const page = await miniProgram.currentPage(); await (await page.$('#username')).input('testuser'); await (await page.$('#password')).input('123456'); await (await page.$('.login-btn')).tap(); // 等待跳转或成功提示 await page.waitForSelector('.login-success-toast', { timeout: 5000 }); console.log('登录流程测试通过'); } catch (error) { console.error('登录流程测试失败:', error.message); // 这里可以截图,方便排查 await page.screenshot({ path: 'login-error.png' }); } }5.2 引入断言库与测试框架
单纯的脚本执行无法判断测试是否“通过”。我们需要引入断言库(如Node.js内置的assert或更强大的chai)来验证结果。
const assert = require('assert').strict; async function testCartCount(page) { await page.navigateTo('/pages/cart/cart'); const countElement = await page.$('.cart-count'); const countText = await countElement.text(); // 断言购物车数量显示为数字 assert(!isNaN(parseInt(countText)), '购物车数量应为数字'); // 断言添加商品后数量增加 const initialCount = parseInt(countText); await addProductToCart(page); await page.waitFor(1000); // 等待界面更新 const newCountElement = await page.$('.cart-count'); const newCount = parseInt(await newCountElement.text()); assert.strictEqual(newCount, initialCount + 1, '添加商品后购物车数量应增加1'); }更进一步,可以集成像Jest或Mocha这样的测试框架。它们能提供测试套件组织、生命周期钩子(beforeAll, afterEach)、测试报告等强大功能。将每个测试用例写成it(‘should …’, async () => {…})的形式,管理起来会非常清晰。
5.3 关键操作封装与页面对象模型(Page Object)
这是提升脚本可维护性的核心设计模式。将针对某个页面的所有操作和元素选择器封装在一个类或对象中。
// pages/HomePage.js class HomePage { constructor(page) { this.page = page; } async navigateTo() { await this.page.navigateTo('/pages/index/index'); return this; } async getSearchInput() { return await this.page.$('.search-input'); } async search(keyword) { const input = await this.getSearchInput(); await input.input(keyword); await (await this.page.$('.search-btn')).tap(); await this.page.waitForSelector('.search-result', { timeout: 3000 }); } async getFirstProductName() { const firstProduct = await this.page.$('.product-item:first-child .name'); return await firstProduct.text(); } } // 在测试用例中使用 const homePage = new HomePage(currentPage); await homePage.navigateTo(); await homePage.search('手机'); const name = await homePage.getFirstProductName(); assert(name.includes('手机'));这样做的好处是,当页面UI改版,只需要修改HomePage.js文件中的选择器,所有测试用例都无需改动,实现了业务逻辑与UI细节的解耦。
6. 常见问题排查与性能优化实录
在实际操作中,我踩过不少坑。这里把一些典型问题和解决方案整理出来,希望能帮你节省时间。
6.1 连接与启动失败问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
launch超时或报错 | 1.cliPath路径错误。2. 开发者工具未登录或有多开冲突。 3. 端口被占用。 | 1. 仔细检查cliPath,Windows注意.bat后缀。2. 关闭所有开发者工具,重新用CLI启动,确保登录态一致。 3. 更换开发者工具中的服务端口号。 |
connect失败,提示超时或无法连接 | 1.wsEndpoint端口号与开发者工具设置不符。2. 开发者工具未开启服务端口。 3. 防火墙或安全软件拦截。 | 1. 核对端口号,确保一致。 2. 进入开发者工具设置,确认“服务端口”已开启。 3. 临时关闭防火墙或添加规则。 |
脚本执行过程中元素找不到 ($或waitForSelector报错) | 1. 页面未加载完成就进行操作。 2. 选择器写错了。 3. 元素是动态生成的,等待时间不足。 4. 页面有 if/else或hidden控制显示。 | 1. 在关键操作前增加page.waitFor(毫秒数)或waitForSelector。2. 使用开发者工具的“选择器”功能验证选择器。 3. 使用 waitForSelector并设置合理的超时时间。4. 确保操作时元素确实处于显示状态。 |
6.2 脚本稳定性与性能优化心得
增加智能等待,避免“硬等待”:到处使用
page.waitFor(3000)虽然简单,但会让测试变慢且不稳定。优先使用waitForSelector、waitForFunction(等待某个JS条件成立)等,它们会在条件满足时立即继续,否则才等到超时。// 不佳 await page.waitFor(3000); await page.$('.btn').tap(); // 更佳 await page.waitForSelector('.btn', { timeout: 5000 }); await (await page.$('.btn')).tap();善用截图功能定位疑难杂症:当脚本在某个步骤失败时,立即对当前页面截图,能直观看到失败时的界面状态,比看日志有效得多。
try { await someOperation(); } catch (error) { const timestamp = new Date().getTime(); await page.screenshot({ path: `error-${timestamp}.png` }); throw error; // 重新抛出错误 }管理好小程序生命周期:每次测试用例开始前,最好能回到一个干净的初始状态。对于小程序,可以调用
miniProgram.reLaunch(‘/pages/index/index’)重启到首页。在测试套件的beforeEach钩子中做这件事。控制测试规模与并行:不要在一个脚本里测试所有功能。按功能模块拆分成多个独立的测试文件。可以考虑使用
Jest的--maxWorkers参数进行有限度的并行测试,但要注意小程序CLI实例的资源占用。
第一次尝试miniprogram-automator的过程,就像是为我的开发工作流打开了一扇新的大门。从最初连接失败的烦躁,到成功写出第一个自动登录脚本的兴奋,再到设计出Page Object模型后的从容,这个过程让我深刻体会到,自动化测试的价值不在于替代所有手工测试,而在于把开发者从重复、机械的验证中解放出来,去关注更复杂的逻辑和用户体验。它更像是一个永不疲倦的、严格执行的质检员。如果你也在进行小程序开发,我强烈建议你抽出半天时间,跟着上面的步骤实践一次。最初的搭建成本,会在后续无数次的回归测试中加倍回报给你。