如何用最小的代价保证前端代码的质量? 前端如何测试? 如何利用测试, 提高开发效率?
背景
众所周知, 要在前端做好单元测试, 是一件困难的事, 或者, 是件吃力不讨好的事, 但并不能说明, 前端测试不重要. 相反, 没有测试的代码, 就少了一道安全的屏障.
相信各位大佬开发时, 也有遇到个, 这么一个问题: 在迭代一个项目需求时, 新功能是没问题, 然而老功能确出bug了, 这种问题, qa同学需要费很大的功夫回归测试一次, 才能测出来.
或者遇到这么一个问题: 在迭代项目时, 要么是不知道接口返回的数据长什么样子, 要么是需要来回更改mock数据.
根据这些问题, 我花了些心思, 想出来一套根治脱发的方案..
单元测试
为啥需要单测? 但你写的一个工具, 很多人都在用, 突然有一天, 你要加个新特性, 还有兼容以前的用法, 就问你敢不敢改?
单测的目的之一, 就在于此: 快速回归, 保证兼容性. 其实后面讲的黑盒测试, 也是这个目的
单测还有个好处: 方便开发
目前只针对部分工具库进行的单元测试.
如果想把项目所有代码加上单测, 也不是不可能, 只是收益不大. 因为前端逻辑代码, 写单测比较困难, 往往测试代码比实际代码量都大. 而且业务多变, 改业务代码的同时还要改更多的测试, 比较痛苦
使用工具
power-assert 断言库
搭配 mocha, assert(testValue === 'your expect!')
如果不相等, 测试失败
mocha 测试库
搭配一个断言库, 就可以愉快的写测试了:
describe('caes: promise', () => {
let yourTool = null
beforeEach((done) => {
// 每个单测初始状态
yourTool = new MyTool({})
done();
})
afterEach(done => {
每个单测完成后
yourTool = null
done();
})
it('your handsome test1', (done) => {
// test some thing
done()
})
it('your handsome test2', (done) => {
// test some thing
done()
})
it('your handsome test3', (done) => {
// test some thing
done()
})
})
问什么需要这个? 如果仅仅是一些简单的工具, 或许看不出来他的厉害之处. 我觉得, 我愿意用他的原因是
- 减少我们的重复工作: 在每次测试之前, 将环境置于初始状态
- 生成测试报告
- 可以搭配断言, 自动判断结果
babel-cli 解析es6语法
这个不是必要的, 你可以选择commonJS+es5的语法写单测, 不冲突
黑盒测试
黑盒测试就是, 手动点. 目前黑盒测试是qa测试的主要方式. 当然, 测试同学, 会打开charles查看接口返回是否正常
黑盒测试的痛点在于数据, 想一想:我们qa同学为了测一个case, 可能要花5分钟造一个数据(反正我是至少要花10分钟), 然后测出前端一个低级bug, qa同学, 心情可能就不那么美丽了…
然而, 我们要测试前端的功能, 是完全可以使用mock数据, 绕开后端测试数据难的问题: 使用 yapi来模拟各种case.
qa同学的测试流程
- 测试流程:
- charls 配置代理
- https://a.com 改成http://110.110.110.110:8088
- https://b.com 改成http://110.110.110.110:9040
- 在 http://110.com/#/testNumber/my/new 申请新账号
- 登录测试包
- 测试包下载地址: http://110.com
- 扫码 http://110.110.110.110:8081/?debug=true&openBridge=true&scene_id=S001#/
- 开通实名 postman
- uid 到110.110.110.110 /home/110/usercenter-api/logs/business.log
- http://110.110.110.110:9999/tools/realname/insert
- {“userId”:”110”,”name”:”测试五”,”certId”:”110”,”status”:”110”}
- 获取token
- 110.110.110.110:8088里面的 header => authoriztion
- 复制到to_page_tes(1)2.html的href, 需要先local map
- 110.110.110.110:9040/checkstand/index.html -> /Users/didi/Documents/to_page_tes(1) 2.html
- 打开app, 点击一键开通
- 测试五:
- 姓名:测试五
- 卡号:1101 1011 0110 1101 10110
- 手机号:110
- 身份证号:110110110110110(110110110测试环境用这个)
- charls 配置代理
通过url来进入调试模式
对于移动端, 我们没有pc端那么好调试, 如何快速进入调试? 我使用url注入参数, 大家可以参考
- url可以调试的字段:
- payfeDebug
- true: 打开调试工具, 后面的useApi, openBridge, debugCase才生效
- 不加默认是false
- useApi: 改变前端的api地址
- zz: rd的测试机, 这样可以省掉配charles的map的成本
- mock: yapi的数据,参考后面yapi部分
- 默认是线上
- openBridge
- true: 使用bridge, 应该在app里打开h5页面, 调试bridge功能
- 在debug模式下, 默认是false, 为了方便测试我们的逻辑, 跳过bridge的相关功能并调起成功的回调
- debugCase
- 各种testcase: 参考后面yapi部分
- payfeDebug
payfeDebug:控制打开调试工具
(function(){
var isDebug = /payfeDebug=true/.test(window.location);
if (!isDebug || (isDebug && localStorage.getItem('active-eruda') === 'false')) return;
var debugScript = document.createElement('script');
debugScript.src='//cdn.jsdelivr.net/npm/eruda';
document.head.append(debugScript);
debugScript.onload = function () {
eruda.init();
eruda.position({x: 20, y: 20});
}
})()
openBridge: 控制使用bridge
if (isDebug && !openBridge) {
const logStyle = 'font-weight: bold; text-decoration: underline';
const call = (method, params, callback = r => r) => {
params.extInfo = _.assignIn(params.extInfo, ext);
console.log('%cFAKE DidiJSBridge call:', logStyle, method, params);
// 根据不同case 来 模拟bridge的结果
// 回传结果可以在console里注入
callback(window.ddpayBridgeCallBackParams || { resultCode: 2 });
};
bridge = {
call,
};
} else {
bridge = {
call(method, params, callback) {
win.Fusion[call](params, callback)
},
};
}
yapi 属于前端的后端
yapi是mock数据的系统, 可以看出一个数据库, 文档自己google
对于前端的各种界面, 就可以对应到后端的数据库数据的各种状态.
基于这个认识, 我们可以想象, 其实一个前端的case对应到后端数据库的状态, 每一个接口都有他的各种状态, 举例:
- 第0个api: /card 接口是 查询 用户 已经绑了哪些卡 的接口, 可能的状态:
- b: 绑了卡, 里面至少有一张快捷卡
- ba: 绑了两张卡, 一张可用
- be: 绑了一张卡, 但 support=false
- n: 没有绑卡
- r: 认证卡, 可以升级快捷卡
- m: 认证卡, 不可升级快捷
同理, 再举一个例子:
- 第1个api: qrcode/check_need_create_qr_pay 签约接口
- s: 签约
- n: 没有签约
然后两接口排列组合成 6 * 2 = 12种case, 每一种case都是用户可能会遇到的
那我怎么表示每种case呢? 使用api下标 + 状态标识 来标识一种case
0b1s: 代表 绑了卡 也签了约 的用户
0n1s: 代表 没绑卡 但签了约 的用户
以此类推…
这些状态可以通过url注入到前端代码, 也可以通过window.zpdebugCase来注入(随便起的名字, 但注意不要和其他全局变量冲突), 最后在http.js里 注入到每个请求的header里(代码见下面); 最后, yapi通过header将这个接口对应状态的数据返回, 见 高级mock
http.js 增加拦截器
// 测试使用, 通过url来测试各个case
// 结合使用env里的debugCase
if (isDebug && params.debugCase && useApi === 'mock') {
io.interceptors.request.use(
(config) => {
config.headers.debugCase = window.zpdebugCase || params.debugCase;
return config;
}
);
}
yapi 高级mock
var caseHeader = header.debugcase || '';
var caseId = caseHeader.match(/(?:3)([a-zA-Z]*)/) || [null, 'o']
switch(caseId[1]) {
case 'n':
mockJson.data = [];
break;
case 'r':
mockJson.data = [认证卡];
break;
}
yapi 来测试前端请求是否正确
yapi不仅可以返回mock数据, 还可以检测前端的请求是否正确
比如校验:
- 前端请求参数类型
- 前端请求参数个数
防止前端因为接口少传参数引起的bug
yapi 来测试后端的接口
理想状态是, 可以mock出用户的请求数据, 然后断言出后端测试的接口是否能正常返回, 拿到结果继续模拟用户发现下一个请求, 直到把后端的case测完
但往往是后端的接口需要测试账号和token, 而这些是不可重复使用的…
nightwatch End-to-End tests
nightwatch是一个前端UI测试框架, 文档自己google哈
有了比较好的黑盒测试的理论基础, 就有可能做 end-to-end测试, 效果就是让程序自动跑一次所有的case
怎么搭配前面的yapi来自动测试? 可以看一段测试代码:
// 新用户开通情景模拟
module.exports = {
/**
* 不同用户状态: 未签约, 已签约为升级, 已签约已升级等
*/
'测试: 新用户开通流程': function(browser) {
browser
// 初始状态: debugCase=0n1n 没绑卡, 没签约
.url('http://localhost:8080/?payfeDebug=true&useApi=mock&debugCase=0n1n&scene_id=S001#/')
.pause(4000)
.waitForElementVisible('body', 1000)
// 点击开通
.assert.containsText('#app', '一键开通')
.click('.md-agree-icon')
.pause(100)
.click('.md-button.primary.large')
// 改变数组库状态, 并绑卡成功
.execute(function(data) {
window.zpdebugCase = '0b1s';
}, [], null)
.pause(100)
.click('.md-dialog-actions a:nth-child(2)')
.pause(1000)
.assert.visible('.SmallBarcode__content')
},
}
解读下步骤:
- 在这之前, 设置beforeEach, 让浏览器的宽度和iphone6一样
- 通过url()来打开测试网页, 并通过 参数 开启调试, 指定用户为未签约未绑卡状态
- 访问mock数据, 根据debugCase=0n1n 返回未签约未绑卡应该返回的数据
- 判断, 页面正常加载出来了
- 模拟用户点了一下同意协议
- 模拟用户点了一下一键开通
- 给window注入zpdebugCase, 让下一次请求的head的debugCase=’0b1s’
- 模拟用户点了绑卡, 因为这里没有开启openBridge, 这个直接调用绑卡成功, 然后到请码页
- 访问mock数据, 根据debugCase=0b1s 返回签约绑卡应该返回的数据
- 判断,请码成功
通过这个方式, 前端几乎可以将代码测试覆盖率达到100%, 你的每一个case都可以自动化测试
其他端到端的选型
selenium, cypress 效果和nightwatch类似
优点
显而易见, 以最小的代价测完前端的case
缺点
如果要使用这种方式, 最容易出错的地方就是, mock数据和后端的真实数据不一致, 这个解决是
- 前端每次迭代时主动和后端联调, 并更新mock数据
- 前后端一起维护一套mock数据
虽然这样是可以避免大部分, 但, 走一次完整的前后端测试流程, 也是不可少的. 比如我们覆盖到的问题
- 手机兼容性问题
- 后端同学偷偷改变了接口
愿景
有了测试, 下一步, 就应该探索前端的工程化, 比如: 每次我提交一次代码, 都会自动构建, 自动测试, 都会生成测试报告, 然后部署在docker机里, 随时也可访问. 如果出错了, 邮件告知, 提前发现问题
还希望看看后端的单测怎么搞的, 是不是可以联合起来?