本文做者:IMWeb IMWeb团队 原文出处:IMWeb社区 未经赞成,禁止转载javascript
微信小程序生态日益完善,不少小程序项目页面愈来愈多,结构愈来愈复杂,业务逻辑也更加多样。以腾讯课堂小程序为例,目前腾讯课堂小程序部分页面结构和不一样业务场景下的表现以下图所示:html
能够看到在核心功能上主要页面对于不一样业务场景有众多不一样的表现,所以在开发与发布的过程当中须要手动验证大量测试用例以保证小程序按预期表现运行,善于利用工具的程序员固然会想:java
这种重复的工做能不能交给程序自动进行呢?node
web开发中对于这类测试问题已经有了不少自动化解决方案好比Selenium、Puppeteer,思路大致相同,都是让浏览器按照指定顺序自动在页面上完成点击、输入等操做,再将操做后的页面表现与想要获得的结果进行比较获得测试结论(断言)。那小程序中有没有一种方案可以按照这种思路实现自动化操做并提供页面信息用于断言呢?为了微信底层安全考虑,小程序环境一直比较封闭,留给开发者操做的余地很小,自动化操做基本没法实现,但5月底出现了miniprogram-automator工具,给了小程序开发者但愿。程序员
基于miniprogram-automator的文档描述简单总结一下,当经过命令打开开发版微信开发者工具的自动化接口并链接自动化接口后,此工具可提供如下能力:web
因此小程序自动化控制的实现依赖于开发版小程序开发者工具以及miniprogram-automator工具。小程序开发者工具命令行用来打开指定自动化操做服务端口。(开发者工具版本需高于v1.02.1906042)。miniprogram-automator工具用来操做开发者工具中运行的小程序并获取所需的信息。对于测试需求能够结合jest框架进行测试用例的组织和断言。npm
很少废话,看完文档用一下:小程序
Ø 调用开发者工具命令行打开项目与指定自动化操做服务端口微信小程序
PS D:\programs\内测\微信web开发者工具> ./cli.bat --auto D:\weApp\testMiniprogram --auto-port 9420
Initializing...
idePortFile: C:\Users\billcui\AppData\Local\微信开发者工具\User Data\Default\.ide
starting ide...
IDE server has started, listening on http://127.0.0.1:35510
initialization finished
Open project with automation enabled success D:\weApp\testMiniprogram
复制代码
这一行命令须要注意的有:浏览器
Open project with automation enabled success D:\weApp\testMiniprogram
这行提示才算是成功打开了自动化端口(9420)。命令运行成功后,开发者工具会自动打开项目,并弹出提示
Ø npm i miniprogram-automator --save-dev
安装SDK,建立test.js,代码中引入miniprogram-automator工具,链接自动化操做端口
const automator = require('miniprogram-automator');
const miniProgram = automator.connect({
wsEndpoint: 'ws://localhost:9420',
})
复制代码
Ø 利用miniprogram-automator提供的接口操做小程序从首页重启并进行相关操做
const automator = require('miniprogram-automator');
const miniProgram = automator.connect({
wsEndpoint: 'ws://localhost:9420',
}).then(async miniProgram => {
// 从首页重启
const page = await miniProgram.reLaunch('/pages/index/index');
// 从页面获取bottom-button组件
const button = await page.$('bottom-button');
// 打印出button的wxml信息
console.log(await button.wxml());
}).catch(e => {
console.log('catch a error', e);
});
复制代码
Ø 利用miniprogram-automator获取操做后页面相关信息,利用jest进行组织和断言
// index.spec.js
const automator = require('miniprogram-automator');
describe('课堂小程序自动化测试', () => {
let miniProgram;
// 运行测试前调用
beforeAll(async () => {
miniProgram = await automator.connect({
wsEndpoint: 'ws://localhost:9420',
});
});
// 运行测试后调用
afterAll(() => {
miniProgram.disconnect();
});
// 测试内容
it('nohost检测', async () => {
const page = await miniProgram.reLaunch('/pages/index/index');
const nohostButton = await page.$('nohost');
expect(nohostButton).toBeNull();
});
});
复制代码
运行jest index.spec.js
, 若是页面中不存在nohost组件则测试经过,结果如图所示:
腾讯课堂微信小程序引入自动化测试主要是为了解决开发、预发布环境、正式环境须要反复屡次打开用例课程页面,操做繁琐,耗费大量人力的问题。针对课堂小程序checklist,尽量利用自动化测试程序完成测试验证,减小手动操做,也能够避免人为检测的遗漏。
利用miniprogram-automator工具和jest框架,自动化测试主要能力为按照指定顺序模拟打开指定页面、点击、滚动等操做和设置page的data渲染数据,而后对特定的页面结构、数据、组件属性等信息进行断言,判断是否符合预期。
下面以腾讯课堂微信小程序的课程详情页为例来详细说明在实际项目中如何实现自动化测试:
课程详情页的UI主要分为视频部分,详情部分以及底部的购买按钮,未购买课程时付费课程详情页表现以下:
假如对于未购买的无优惠活动的付费课程详情页的测试目标以下:
实现这个测试,在x.spec.js
文件中首先须要要按照上文的步骤引入miniprogram-automator,在beforeAll中链接已经打开自动化端口的微信小程序项目。(这里再也不重复代码,见上一章)下面直接看测试内容的代码。
// 打开页面,经过url传参
const page = await miniProgram.reLaunch(`/pages/course/course?cid=${commonPayCid}`);
// 获取按钮组件信息
const basicApplyButton = await page.$('.basic--buy');
// 判断按钮显示内容
expect(await basicApplyButton.wxml()).toContain('当即购买');
// 模拟点击按钮
await basicApplyButton.tap();
// 等待页面跳转
await page.waitFor(1500);
// 获取当前页面路径
const currentPage = await miniProgram.currentPage();
// 判断跳转后路径是否正确
expect(currentPage.path).toContain('pages/order/order');
// 跳转回来
await miniProgram.navigateBack();
复制代码
目前miniprogram-automator提供了两种方法获取到页面中的组件:page.$()
和page.$$()
通过实验发现二者的selector支持经过组件名和类名选择组件,但对于自定义组件内部的结构,就不能直接这样拿到了。
课程详情页的底部按钮实际上是一个自定义组件,而且还嵌套了子自定义组件,咱们看一下底部按钮的wxml结构:
红色框框就是想要获取的目标,尝试一下直接经过page.$('.bottom-btn')
或page.$('.buy')
返回的都是undefined,那怎么获取呢?咱们先来看看bottom-button内部是什么样子的。
const basicApplyButton = await page.$('bottom-button');
console.log(await basicApplyButton.wxml());
复制代码
获取bottom-button并打印它的wxml字符串看一下:
// 输出其实是字符串,为了方便显示格式化了一下
<view class="bottom-button--bottom-button-space" wx:nodeid="17">
<view class="bottom-button--bottom-button-wrapper" wx:nodeid="261">
<basic is="components/discount-button/components/basic/basic" wx:nodeid="262">
<view wx:nodeid="263">
<view class="basic--bottom-button-container" wx:nodeid="264">
<view class="basic--bottom-btn basic--buy" wx:nodeid="265">当即购买</view>
</view>
</view>
</basic>
</view>
</view>
复制代码
发现了什么!小程序实际运行时,自定义组件内部的类名都加上了组件名前缀,再试试page.$('.basic--buy')
发现果真成功获取到了,因此虽然表面上miniprogram-automator只能操做和获取page中的内容,但自定义组件内部的结构实际上也是以某种方式存在于page中的。
接下来看一下跳转,能够直接获取到对应组件后调用.tap()
方法来模拟点击,这里须要注意的是,因为微信小程序开发者工具中点击打开新页面耗时较长,须要等待页面加载一会,否则接下来获取当前页面路径的时候页面还没跳转过去就拿不到不到新页面路径了。等待的时长能够根据经验给个稍大的比较安全的值。
const player_video = await tapTcplayer(page, '.player-task');
expect(await player_video.wxml()).toContain('video-current-time'); // 试学
复制代码
因为微信开发者工具的限制,云点播会降级为tcplayer播放,tcplayer内部的核心组件实际上是<video>
组件,wxml结构以下:
如何判断视频是否成功播放呢?
咱们先按照上面的方法获取播放成功的video组件的wxml字符串看看
"<video class="component-video-video--player_video" controls="" danmu-list="[]" initial-time="0" object-fit="contain" poster="https://10.url.cn/qqc..." src="http://113.96.98.148/vedu.tc.qq.com/AtmkzyWCuq..." autoplay="" wx:nodeid="446"><div class="video-container" wx:nodeid="447"><div class="video-bar full" style="opacity: 1;" wx:nodeid="457"><div class="video-controls" wx:nodeid="458"><div class="video-control-button pause" wx:nodeid="459"><div parse-text-content="" class="video-current-time" wx:nodeid="460">00:02<div class="video-progress-container" wx:nodeid="462"><div class="video-progress" wx:nodeid="463"><div style="left: -21px;" class="video-ball" wx:nodeid="464"><div class="video-inner" wx:nodeid="465"><div parse-text-content="" class="video-duration" wx:nodeid="466">06:09<div class="video-fullscreen" wx:nodeid="468"><div style="z-index: -9999" class="video-danmu" wx:nodeid="453"></video>"
复制代码
惊了!原生<video>
组件内部居然是<div>
,咱们还能够注意到一个关键的class: video-current-time 内部数值为00:02,这不是当前播放进度吗?恰好能够用来判断视频有没有播放成功,就是它了!
对比发现播放失败时根本不会出现class为video-current-time的div,因此直接用是否包含video-current-time来判断了。
点击非试看课程时,没法播放视频。因为不播放视频时页面中只显示cover封面图,不attach <video>
组件,因此直接用获取视频组件的结果进行toBeNull()判断便可。结合上面全部的代码以下:
async function tapTcplayer(page, className = '.task-item') {
const taskItem = await page.$(className);
await taskItem.tap();
await page.waitFor(3000);
const playercover = await page.$('.player-cover');
const player_video = await playercover.$('.component-video-video--player_video');
return player_video;
}
it('付费课程详情页按钮显示、跳转、点播、试学功能测试', async () => {
const page = await miniProgram.reLaunch(`/pages/course/course?cid=${commonPayCid}`);
const basicApplyButton = await page.$('.basic--buy');
expect(await basicApplyButton.wxml()).toContain('当即购买'); // 按钮显示
await basicApplyButton.tap();
await page.waitFor(1500);
const currentPage = await miniProgram.currentPage();
expect(currentPage.path).toContain('pages/order/order');
await miniProgram.navigateBack();
const player_video = await tapTcplayer(page);
expect(player_video).toBeNull(); // 未报名不能播放视频
const player_video_new = await tapTcplayer(page, '.player-task');
expect(await player_video_new.wxml()).toContain('current'); // 试学
}, 20000);
复制代码
能够看到实际上先测试了播放课程功能,再测试了试学功能,这是为何呢?
这是一个坑:因为播放课程失败时会有showModel弹窗提示,这个弹窗是不在wxml结构中的,没法用自动化控制工具点击关闭,实际测试中这个弹窗会阻塞下一个测试项的第一步:页面跳转,致使下一个测试项直接打不开页面致使失败,只能等待一段时间再跳转,因此直接把弹窗放在测试试学功能以前,就不会影响下一个测试项了。
还有一个须要注意的地方,在项目中,点击播放后5秒不触发进度刷新的方法就会上报视频播放失败,实际测试发现通常3秒便可正常播放,因此只等待3秒,3秒后未成功播放的视为播放失败。
最后,jest默认一个测试项的时长不能大于5秒,这项测试既有页面跳转又有视频播放,明显会超出5秒的限制,实际耗时约为15秒左右,因此修改时长限制为20000毫秒。
运行测试脚本结果以下:
目前实现的测试功能以下:
checklist中功能测试的完成状况以下:完成度为65%
review点 | 自动化测试 | 备注 |
---|---|---|
是否去除nohost插件 | 支持 | |
首页是否正常显示 | 支持 | |
pc首页小程序登录是否正常 | 暂不 | 信息受权没法自动完成 |
安卓支付能力是否正常 | 暂不 | webview内部没法获取信息 |
分类页是否正常显示 | 支持 | |
是否能够正常登录 | 暂不 | 信息受权没法自动完成 |
课程表是否正常展现,学习进度/直播状态是否正常显示 | 支持 | 待完善 |
课程详情页是否能够正常展现 | 支持 | |
扫码/分享是否正常唤起小程序 | 暂不 | 开发者工具不支持 |
付费课直播是否能够正常播放(上云跟腾讯视频) | 暂不 | 开发者工具不支持直播 |
免费课直播是否能够正常播放(上云跟腾讯视频) | 暂不 | 开发者工具不支持直播 |
免费课录播是否能够正常播放(上云跟腾讯视频) | 部分支持 | 开发者工具降级到tcplayer |
付费课录播是否能够正常播放(上云跟腾讯视频) | 部分支持 | 开发者工具降级到tcplayer |
试学任务是否能够正常播放 | 支持 | |
详情页视频是否正常播放 | 支持 | |
营销工具相关显示是否正常 | 支持 | |
是否能正常完成支付逻辑 | 暂不 | webview内部没法获取信息 |
类目筛选是否正常 | 支持 | 待完善 |
是否能够正常搜索且列表显示正常 | 支持 | 待完善 |
本地加载耗时是否保持1s内 | 支持 |
page.$()
或page.$$()
方法,经尝试选择器仅支持组件名和类名。没法直接获取自定义组件内部组件元素,须要在类名前增长前缀。实际项目的页面中大量使用自定义组件,对于自定义组件内部的结构判断很是不方便,只能经过wxml()
方法将自定义组件内部结构打印出来才能确认内部的子组件的实际状况。且没法调用自定义组件内部的方法。<web-view>
组件获取不到任何内部信息,也没法自动化控制。但愿这些问题后续可以获得解决~~