msdlisper Front-end

前端自动化测试

2018-11-27

如何用最小的代价保证前端代码的质量? 前端如何测试? 如何利用测试, 提高开发效率?

背景

众所周知, 要在前端做好单元测试, 是一件困难的事, 或者, 是件吃力不讨好的事, 但并不能说明, 前端测试不重要. 相反, 没有测试的代码, 就少了一道安全的屏障.

相信各位大佬开发时, 也有遇到个, 这么一个问题: 在迭代一个项目需求时, 新功能是没问题, 然而老功能确出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测试环境用这个)

通过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:控制打开调试工具

(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机里, 随时也可访问. 如果出错了, 邮件告知, 提前发现问题

还希望看看后端的单测怎么搞的, 是不是可以联合起来?


Comments

Content