谈小程序自动化
在浏览器领域,我们有selenium和puppeteer这样的库,可以自动控制浏览器执行自动化脚本,完成自动化端到端测试、定时自动化任务等等。随着持续集成和持续部署的需求越来越大,即CI/CD,自动化已经成为不可或缺的一部分。
对于日益增长的小程序开发需求,能否自动控制小程序,进而实现自动测试、自动发布等任务?
对于微信小程序,从2019年5月开始,微信官方也开始提供官方自动SDK: SDK:miniprogram-automator。这是一个通过NodeJS在远程实机控制开发者工具和微信的SDK。通过这个SDK,可以控制小程序跳转到指定页面,获取小程序页面数据,获取小程序页面元素状态,触发小程序元素绑定事件,将代码片段注入AppService,调用wx对象上的任意接口等等。
这个SDK通过脚本控制原生的微信开发者工具,近似达到自动测试业务的目的,同时也可以通过远程控制真机来达到真机测试的目的。
原理与初步体验
我们先来体验一下这个SDK。
首先需要确定你安装了微信开发者工具,版本大于1.02.1907232,并且把你的基础库版本设置在2.7.3以上,请安装NodeJS 8.0以上的版本。
我们知道,微信开发者工具为外部调用提供了命令行和HTTP服务两个接口。开发人员可以通过命令行或HTTP请求指令工具进行登录、预览和上传。
通过命令行SDK调用微信开发者工具,然后通过外部手段导入目标项目。微信开发者工具通过读取目标项目的project.config.js来初始化项目。并读取启动命令的- auto-port参数,使SDK可以通过这个端口的Websocket服务与对应的目标小程序调试窗口进行交互。
这是微型程序自动机的一般原理。
感兴趣的读者可以尝试使用微信开发者工具的cli方法运行这个命令。
CLI-项目{项目路径}-自动端口{ WebSocket的端口}
为了运行上面的命令,我们需要找到微信开发者工具的安装目录。不同的操作系统有不同的位置。Mac位于:安装路径/Contents/MacOS/cli,windows位于:安装路径/cli.bat,对于经常使用的读者,建议将cli所在的目录放在系统的环境变量中。
您可能会遇到IDE服务超时的情况。因此,为了保证开发人员工具可以通过命令行打开,需要打开开发人员工具的HTTP服务调用接口。
打开的方式是进入微信开发者工具,选择:设置安全设置。选择:在服务端口中打开。此时,微信开发者工具会自动指定一个可用的端口号。
细心的读者会在这里找到另一个“端口”。与上面提到的端口不同,这个端口是IDE提供外部服务的端口。如上图,36146是IDE服务的端口。在你启动IDE后,你可以访问http://127.0.0.1:36146
您的集成开发环境将专注于您。3358127 . 0 . 0 . 1:36146/是IDE服务的根域名,open是命令。读者可以参考命令索引,通过不同的网址发出不同的命令。
好了,安装了SDK,让我们练习一下:
首先,我们初始化一个npm库,比如auto。你可以通过以下方式安装迷你程序自动机:NPM一号迷你程序自动机保存开发或纱线添加迷你程序自动机开发。
接下来,我们下载一个微信示例程序,在~/demo-miniapp/
接下来,我们创建一个新文件,如index.js,内容如下:
const automator=require(' mini program-automator ')automator . launch({ project path : ' ~/demo-miniapp/',//项目文件地址})。然后(异步迷你程序={constpage=等待迷你程序。重新启动('/page/tabbar/component/index ')等待页面。waitfor (500)常量元素
= await page.$('.kind-list-text') console.log(await element.attribute('class')) await element.tap() await page.waitFor(500) await miniProgram.close()})现在,让我们运行起来。node index.js。我们看到,IDE自动启动,并加载了我们的项目文件,并自动地点开了第一个项目,过一段时间之后,程序自动的退出。
这就是我们对于自动运行的初步体验。
API组成
截至目前(2020年4月初)最新的SDK的API主要分四个模块:Automater、MiniProgram、Page和Element等。
Automator 模块提供了启动及连接开发者工具的方法。开发者可以对连接地址、端口号、项目路径等作出设置。归根结底是对于cli的包装。详见:Automator
MiniProgram提供对小程序的控制。提供以下几类支持,详见:MiniProgram:
- 路由方法。控制小程序的跳转
- 系统信息。与API的wx.getSystemInfo等价
- 转调、mock以及恢复微信API对象wx上的方法
- 在APP对象上注入方法、向小程序暴露方法
- 截图、滚动等方法
- 测试真机、截图、测试账号、关闭等方法
- 打印事件、报错事件
Page 模块提供了控制小程序页面的方法。提供以下几类支持,详见:Page:
- 页面路径方法
- 页面元素选取方法
- 页面元素/逻辑钩子方法
- 页面数组方法
- 页面行为方法
- 页面方法调用代理
Element 模块提供了控制小程序页面元素的方法。提供以下几类支持,详见:Element:
- 元素本身属性获取方法
- 元素子代与后继选择器方法
- 元素事件触发方法
- 元素数据访问方法
- 元素方法访问代理方法
可以看出,除Automator之外,每个API模块都在自己的领域内提供对小程序自身内容的访问特性以及扩充特性。这比较类似于Pupputeer的API设定。
与测试框架的整合
miniprogram-automator本身不提供测试框架,我们可以选用熟悉的测试框架与之整合。这里我们以jest为例。其他诸如mocha、jasmine、Cucumber都比较类似。
现在,在我们之前的项目auto里安装jest。npm i jest -g或yarn global add jest
简单科普下jest的工作原理。在项目中,jest识别三种测试文件:
- 以.test.js结尾的文件
- 以.spec.js结尾的文件
- 放到tests文件夹中的文件。
Jest 在进行测试的时候,它会在整个项目进行查找,只要碰到这三种文件它都会执行。Jest有以下设定:
- 一个describe块称为一个“测试套件”。
- 一个it/test块,称为“测试用例”。测试用例是测试的最小单位。
- 每个测试文件应至少包含一个describe或一个it/test块。
- 一个describe块应至少包含一个或多个it/test块。
- 每个测试用例,可以组合各种断言来判定是否符合预期。
Jest 测试提供了一些测试的生命周期 API。可以辅助我们在每个 case 的开始和结束做一些处理。 这样,在进行一些和数据相关的测试时,可以在测试前准备一些数据,在测试后,清理测试数据。 4 个主要的生命周期函数:
- afterAll(fn, timeout): 当前文件中的所有测试执行完成后执行 fn, 如果 fn 是 promise,jest 会等待 timeout 毫秒,默认 5000
- afterEach(fn, timeout): 每个 test 执行完后执行 fn,timeout 含义同上
- beforeAll(fn, timeout): 同 afterAll,不同之处在于在所有测试开始前执行
- beforeEach(fn, timeout): 同 afterEach,不同之处在于在每个测试开始前执行
回到miniprogram-automator。我们可以在auto项目中加入一个index.spec.js文件。
const automator = require('miniprogram-automator')let miniProgram, pagebeforeAll(async () => { miniProgram = await automator.launch({ projectPath: '/Users/liuguanyu/devspace/demo-miniapp/' }) page = await miniProgram.reLaunch('/page/tabBar/component/index') await page.waitFor(500)}, 50000)afterAll(async () => { await miniProgram.close()})
现在,你在auto下运行jest会报错。
意思是需要我们增加至少一个测试套件或测试用例。
为此我们增加一个测试套件,并增加一些测试用例,修改如下:
const automator = require('miniprogram-automator')let miniProgram, pagebeforeAll(async () => { miniProgram = await automator.launch({ projectPath: '/Users/liuguanyu/devspace/demo-miniapp/' }) page = await miniProgram.reLaunch('/page/tabBar/component/index') await page.waitFor(500)}, 50000)describe("测试微信小程序", () => { // 1. 测试顶部描述 it("标题栏", async () => { const desc = await page.$('.index-desc') // 要求测试标签名必须为view expect(desc.tagName).toBe('view') // 要求测试内容包含文字以下将展示小程序官方组件能力 expect(await desc.text()).toContain('以下将展示小程序官方组件能力') }) // 2. 测试列表项 it('列表项', async () => { const lists = await page.$$('.kind-list-item') // 测试共有7个列表项 expect(lists.length).toBe(7) const list = await lists[0].$('.kind-list-item-hd') //第一个列表元素的标题应该是“视图窗器” expect(await list.text()).toBe('视图容器') }) // 3. 测试列表项行为 it('列表行为', async () => { const listHead = await page.$('.kind-list-item-hd') // 点击应展开未展开项 expect(await listHead.attribute('class')).toBe('kind-list-item-hd') await listHead.tap() await page.waitFor(200) expect(await listHead.attribute('class')).toBe( 'kind-list-item-hd kind-list-item-hd-show', ) // 再次点击应合上 await listHead.tap() await page.waitFor(200) expect(await listHead.attribute('class')).toBe('kind-list-item-hd') // 点击子列表项应该会跳转到指定页面 await listHead.tap() await page.waitFor(200) const item = await page.$('.index-bd navigator') await item.tap() await page.waitFor(1500) expect((await miniProgram.currentPage()).path).toBe('page/component/pages/view/view') }) // 4. 验证wxml方法和setData方法及快照比对 it('验证WXML', async () => { const element = await page.$('page') expect(await element.wxml()).toMatchSnapshot() await page.setData({ list: [] }) expect(await element.wxml()).toMatchSnapshot() }) // 5. mock方法测试并还原 it('伪造请求结果', async () => { // 伪造请求数据 const mockData = [{ rule: 'testRequest', result: { data: 'test', cookies: [], header: {}, statusCode: 200, } }] // mock方法 await miniProgram.mockWxMethod( 'request', function (obj, data) { for (let i = 0, len = data.length; i < len; i++) { const item = data[i] const rule = new RegExp(item.rule) if (rule.test(obj.url)) { return item.result } // 没命中规则的真实访问后台 return new Promise(resolve => { obj.success = res => resolve(res) obj.fail = res => resolve(res) this.origin(obj) }) } }, mockData ) // 请求mock的方法 const result = await miniProgram.callWxMethod('request', { url: 'https://14592619.qcloud.la/testRequest', }) expect(result.data).toBe('test') // 还原方法 await miniProgram.restoreWxMethod('request') }, 30000)})afterAll(async () => { await miniProgram.close()})
此时,我们在auto目录下再次运行jest,则得到如下结果:
我们看到所有的测试都已通过。
真机测试
真机测试可以自动测试以及扫码测试。此时可以在beforeAll里面加入await miniProgram.remote(true)。这个true如果不写,就需要用真机扫码测试。当编译好之后,开发者工具会自动将小程序和调试工具发送到真机。并在侧边增加了测试条。
当不写true时候,运行到remote时,会弹出这样的对话框:
小结
发布3年多,微信小程序已经从微信生态的一环,逐步向多领域渗透。随着开发者的日益增多,面向开发的工具也逐步完善。本文试图管中窥豹,给大家介绍了微信自动化的主要环节。时至今日,小程序已经成长为一种重要的产品形式和生态环境。越来越多的小程序平台正在自己的领域以不同的形式给小程序这个产品形式添砖加瓦。小程序生态和开发环节的完善和成长,也需要广大平台、开发者共同努力,将这一Created in China 的生态体系发扬光大。