单元测试是一个老生常谈的话题,基于 Web/NodeJs 环境的测试框架、测试教程数不胜数,也趋于成熟了。但是对于微信小程序的单元测试,目前还是处于起步状态,这两天在研究微信小程序的测试,也遇到了一些坑,在这里记录一下,希望给看到本文的小伙伴带来一点帮助,少走一些弯路。
本文内容有点多,但是干货满满,不明白的小伙伴可以关注公众号给我留言
demo 地址
https://github.com/xialeistudio/miniprogram-unit-test-demo
关键依赖版本
本文写作时相关依赖版本如下(版本不同,源码行数可能不同):
- miniprogram-simulate: 1.0.7
- j-component: 1.1.6
- miniprogram-exparser: 0.0.6
测试流程
- 初始化小程序项目,编写待测试组件
- 安装 jest,miniprogram-simulate 测试环境
- 编写测试用例
- 执行测试
初始化小程序项目
- 使用小程序开发者工具初始化新项目,APPID 选择
测试号
即可,语言选择Javascript
。 - 使用小程序开发者工具新建
/components/user
组件 -
components/user.js
// components/user.js Component({ data: {nickname: ''}, methods: {handleUserInfo: function(e) {this.setData({ nickname: e.detail.userInfo.nickName}) } } })
-
components/user.wxml
<text class="nickname">{{nickname}}</text> <button class="button" open-type="getUserInfo" bindgetuserinfo="handleUserInfo">Oauth</button>
-
pages/index/index.js
Page({data:{} })
-
pages/index/index.wxml
<view class="container"> <user></user> </view>
- 打开小程序开发者工具,可以看到有一个
Oauth
按钮,点击之后会在上面显示昵称。 - 由此可以得到测试用例
点击授权按钮时上方显示为授权用户的昵称
安装 jest/miniprogram-simulate 测试环境
- 由于 JS 项目的小程序根目录没有
package.json
,需要手动生成一下 - 打开终端,在项目根目录执行
npm init -y
生成package.json
- 安装测试工具集
npm install jest miniprogram-simulate --save-dev
-
编辑
package.json
,在scripts
新建test
命令{ "name": "unit-test-demo", "version": "1.0.0", "description": "","main":"app.js","scripts": {"test":"jest"},"keywords": [],"author":"", "license": "ISC", "devDependencies": { "jest": "^24.8.0", "miniprogram-simulate": "^1.0.7" } }
编写测试用例
- 在项目根目录新建
tests/components/user.spec.js
文件(目录需要手动创建) -
代码如下(参考微信官方单元测试文档编写):
const simulate = require('miniprogram-simulate'); const path = require('path'); test('components/user', (done) => { // 定义测试名称, 传入 done 表示当前测试是异步测试,需要回调函数来告诉 jest,我测试执行完毕 const id = simulate.load(path.join(__dirname, '../../components/user')); // 加载组件 const component = simulate.render(id); // 渲染组件 const text = component.querySelector('.nickname'); // 获取 nickname 节点 const button = component.querySelector('.button'); // 获取 button 节点 button.dispatchEvent('getuserinfo', { // 模拟触发事件 detail: { // 传递事件参数 userInfo: {nickName: 'hello',}, }, }); setTimeout(() => { // 异步断言 expect(text.dom.innerHTML).toBe('hello'); // 检测 text 节点的 innerHTML 等于模拟授权获取的昵称 done();}, 1000); });
执行测试
-
npm run test
,等待一秒后发现,不出意外的话,测试肯定过不去
-
部分出错日志:
Expected: "hello" Received: "" at toBe (/Users/xialeistudio/WeChatProjects/unit-test-demo/tests/components/user.spec.js:18:32) at Timeout.callback [as _onTimeout] (/Users/xialeistudio/WeChatProjects/unit-test-demo/node_modules/jsdom/lib/jsdom/browser/Window.js:678:19) at listOnTimeout (internal/timers.js:535:17) at processTimers (internal/timers.js:479:7)
-
可以推测一下原因:
- dispatchEvent 的事件触发有问题,导致 handleUserInfo 未触发[1]
- dispatchEvent 的事件触发成功,但是触发参数有问题[2]
错误分析(源码跟踪过程)
-
针对第 1 点原因,可以写一下测试代码(
components/user.js
)Component({ data: {nickname: ''}, methods: {handleUserInfo: function(e) {console.log(e); } } })
-
npm run test
,可以看到事件还是成功触发了,不过detail
是{}
console.log components/user.js:21 { type: 'getuserinfo', timeStamp: 948, target: {id: '', offsetLeft: 0, offsetTop: 0, dataset: {} }, currentTarget: {id: '', offsetLeft: 0, offsetTop: 0, dataset: {} }, detail: {}, touches: {}, changedTouches: {}}
- 原因 1 排除,查原因 2
-
dispatchEvent
方法是被测试组件的子组件
,被测试组件
由simulate.render
函数返回 - 浏览
node_modules/miniprogram-simulate/src/index.js
,看到render 函数(152 行)
,可以看到返回的组件由jComponent.create
提供 - 浏览
node_modules/j-component/src/index.js
的create
函数,可以看到其返回了RootComponent
实例,而RootComponent
是由./render/component.js
提供 - 浏览
node_modules/j-component/src/render/component.js
的dispatchEvent
函数,在这里可以打下日志测试 (本文就不打了,结果是这里的 options 就是user.spec.js
dispatchEvent
函数的第二个参数
,detail
是有值的) -
继续跟踪源码,由于咱们的是
自定义事件
,所以会走到91 行
的代码,该代码块如下:// 自定义事件 const customEvent = new CustomEvent(eventName, options); // 模拟异步情况 setTimeout(() => {dom.dispatchEvent(customEvent); exparser.Event.dispatchEvent(customEvent.target, exparser.Event.create(eventName, {}, { originalEvent: customEvent, bubbles: true, capturePhase: true, composed: true, extraFields: {touches: options.touches || {}, changedTouches: options.changedTouches || {},}, })); }, 0);
- 可以看到调用了
exparser.Event.dispatchEvent
函数,该函数的第二个参数
调用了exparser.Event.create
对自定义事件进行了包装,这里还没到最底层,需要继续跟踪 -
exparser
对象是miniprogram-exparser 模块
提供的,浏览node_modules/miniprogram-exparser/exparser.min.js
,发现该文件被混淆了,不过没关系混淆后的代码逻辑是不变的,只不过变量名变得无意义,可读性变差
- 使用 webstorm 格式化该文件,这里我传了一份格式化好的到 github wxparser.js,可在线观看
-
需要在源码中搜索
三个参数
的create
函数(Object.create 不算
),需要有耐心,经过排查后发现 168 行代码应该是目标代码i.create = function(e, t, r) {r = r || {}; var n = r.originalEvent, o = r.extraFields || {}, a = Date.now() - l, s = new i; s.currentTarget = null, s.type = e, s.timeStamp = a, s.mark = null, s.detail = t, s.bubbles = !!r.bubbles, s.composed = !!r.composed, s.__originalEvent = n, s.__hasCapture = !!r.capturePhase, s.__stopped = !1, s.__dispatched = !1; for (var u in o) s[u] = o[u]; return s; }
-
可以看到
s.detail = t
这个赋值,t
是create
的第二个参数
,由node_modules/j-component/render/component.js
的wxparser.Event.create
传入,但是传入的第二个参数写死了 {}
,所以咱们的组件获取detail
的时候永远为 {}
,将其修改为options.detail||{}
即可,修改后代码如下:exparser.Event.dispatchEvent(customEvent.target, exparser.Event.create(eventName, options.detail||{}, xxxxxx
-
重新测试
PASS tests/components/user.spec.js ✓ components/user (1099ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 3.622s Ran all test suites.
避坑指南
-
querySelector
用法同 HTML,但是需要在组件
执行,而不是组件.dom
,HTML 中实在DOMNode
执行的 -
dispatchEvent
是触发事件,需要在组件
执行,上述代码中是触发button 组件
的自定义事件
-
dispatchEvent
事件名规范:去掉前导 bind 剩余的字符串为事件名
,示例代码中bindgetuserinfo
,触发时就是getuserinfo
,如果是bindtap
,那触发时就是tap
-
dispatchEvent
底层是j-component
这个npm 模块实现
的,跟踪源码发现执行是异步的(代码文件node_modules/j-component/src/render/component.js
,函数名dispatchEvent
)// 自定义事件 const customEvent = new CustomEvent(eventName, options); // 模拟异步情况 setTimeout(() => {dom.dispatchEvent(customEvent); exparser.Event.dispatchEvent(customEvent.target, exparser.Event.create(eventName, {}, { originalEvent: customEvent, bubbles: true, capturePhase: true, composed: true, extraFields: {touches: options.touches || {}, changedTouches: options.changedTouches || {},}, })); }, 0);
- 由于
setTimeout
的存在,触发事件为异步,所以写断言时需要加定时器
结语
小程序单元测试基本是没什么经验扩借鉴,但是基于官网提供的工具,以及 开源
,咱们遇到问题时细心排查然后修改一下,还是可以解决问题的。对单元测试有疑问的小伙伴可以扫码加我进行交流