Tiga 前端自动化测试实战分享

陈隆德

前端开发工程师岗

前端动化测试的难点

不同于API的自动化测试,Web/小程序前端自动化测试面临以下问题:

  1. H5和小程序运行时不一致,需要维护两套代码
  2. 页面元素多,没有精力去维护各个组件渲染的测试用例
  3. 不同系统不同版本兼容性不一致
  4. 移动设备种类多,成千上万种屏幕尺寸&分辨率

跨端自动化测试框架 —— Tiga

大致原理见下图,基于puppeteer和automator,将两端的API抹平,同时提供辅助的AnyProxy代理等工具,详情见官方文档:

http://bee.jd.com/d/tiga-doc/

Tiga大致原理见下图: 图片来自Tiga官方文档

基于Tiga扩展插件

Template是Tiga作为开放式框架的重点支点,基于Template,我们可以将用户常用测试场景,抽象出一套通用测试逻辑,免去重复书写的繁琐

一、模版化的优势

1. 更轻量

去除了对Jest的强依赖,断言可以使用任意(BDD/TDD风格)的断言库,例如 jest.expect,chai.should

  • BDD:Behavior Driven Development(expect/should)

行为驱动开发,是一种敏捷软件开发的技术,它鼓励软件项目中的开发,QA和非技术人员之间的协作。主要是以开发特性为视角,更适合开发人员进行单测等。

// 预期 XXX 会有 XXX 的行为/结果,一般都支持链式调用
expect(props.length).toBe(1)
  • TDD:Test-Driven Development(Assert)

测试驱动开发,是一种测试先于编写代码的思想用于指导软件开发,测试驱动开发是敏捷开发中的一项核心时间和技术,也是一种设计方法论,TDD的思想是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。

// 断定XX是XX
assert.lengthOf(props.length, 3, 'foo`s value has a length of 3');

2. 开放性/扩展性

基于Tapable的插件模式诞生于webpack插件化,Tiga借鉴了Tapable插件化的思路,用户可以更方便地进行扩展,兼容更多场景。暴露的hooks如下:

二、实际编写用例时遇到的问题

问题1: 首次http请求没有拦截

addHttpRule逻辑是先访问页面,再注入httpRule规则

  1. 生成page,page访问对应页面
  2. page中注入httpRule
  3. 当前page执行reload

可能出现的场景是,在第一步goto页面中时,一些页面可能调用接口返回未登录,直接跳登录了

问题2: 默认不支持JSONP

addHttpRule默认没有支持jsonp请求,没有返回请求头相关信息,因此data也没法通过function来return数据

问题3: imageMatcher不支持图片缩放对比

小程序Automator截图是根据屏幕dpr进行“真截图”,以Mac外接1080P显示器为例:

  • 在视网膜屏下,小程序automator会截图出 750px 的图片,与H5的Puppeteer表现一致
  • 在外接1080P显示器下运行,小程序截图只有 375px 的图片

三、自行扩展插件解决

基于开发式的插件模式,我们参考http-mock对Tiga的能力进行扩展,依次解决上述业务实际使用的痛点:

0. 导出一个插件类

插件核心就是导出一个类,这个类中实现约定好的钩子函数即可

class HttpMockRequestPlugin {
constructor() {
}
async beforeGo(url, store) {
}
async beforeAllSpecs(store) {
}
async beforeEachSpec(spec, store) {
}
async onAssert(spec, expect, store) {
}
async afterEachSpec(store) {
}
}

1. 支持第一次请求就拦截mock

参考http-mock实现,修改Page导航的时机

async beforeAllSpecs(store) {
// 在用例执行前生成page,但是不做页面跳转
store.page = await store.app._parent.newPage()
}
async beforeEachSpec(spec, store) {
// 在每个用例执行前,先注册当面用例的http规则
this.ruleIds = await store.page.addHttpRules(rules)
// 然后再调用goto做页面跳转
await store.page._parent.goto(spec.pageUrl || store.global._pageUrl)
// 这样页面的第一次请求就会被拦截到
}

2. 支持JSONP

读取请求头的url中的callback参数,在数据处理时拼接字符串即可

static async onRequest(req) {
// 从链接从读取callback参数
const jsonpCallback = getJSONPCallback(req.url())
const formatRes = (res) => {
try {
if (Object.prototype.toString.call(body) === '[object Object]') {
body = JSON.stringify(body)
if (jsonpCallback) {
// 数据序列化后,拼接jsonp方法
body = `${jsonpCallback}(${body})`
}
}
} catch (e) {
// ...
}
return Object.assign({}, res, {
body,
})
}
}

3. 图片对比处理支持缩放

视网膜屏是750px的图,普通屏幕是375px的图,因此从准确性看,需要将750px的图片数据缩放成375px版本的数据,然后再进行对比

通过阅读源码我们发现:pngjs处理后的data是一个 number[],每4个值代表1个点

// 每4个代表1个点 data = [r0, g0, b0, a0, r1, g1, b1, a1 //...]

因此可以通过放大比例,取到放大区域的四个顶点,将四个顶点的RGBA相加取平均数得到缩放后的rgba

const createImageResizer = (width, height) => (source) => {
const scaleX = source.width / width;
const deltaX = source.width / width - 1;
const scaleY = source.height / height;
const deltaY = source.height / height - 1;
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
// 放大2倍取四个顶点的rgba数值
// 12
// 34
const idx1 = ((source.width * y * scaleY) + x * scaleX) << 2;
const idx2 = ((source.width * y * scaleY) + x * scaleX + deltaX) << 2;
const idx3 = ((source.width * (y * scaleY + deltaY)) + x * scaleX) << 2;
const idx4 = ((source.width * (y * scaleY + deltaY)) + x * scaleX + deltaX) << 2;
// 取平均值,作为缩放后的色值
const r = (source.data[idx1] + source.data[idx2] + source.data[idx3] + source.data[idx4]) / 4
const g = (source.data[idx1 + 1] + source.data[idx2 + 1] + source.data[idx3 + 1] + source.data[idx4 + 1]) / 4
const b = (source.data[idx1 + 2] + source.data[idx2 + 2] + source.data[idx3 + 2] + source.data[idx4 + 2]) / 4
const a = (source.data[idx1 + 3] + source.data[idx2 + 3] + source.data[idx3 + 3] + source.data[idx4 + 3]) / 4
// idx 缩放后的位置 <<2 代表乘以4(每个点需要4个值)
const idx = ((source.width * y) + x) << 2;
source.data[idx] = r;
source.data[idx + 1] = g;
source.data[idx + 2] = b;
source.data[idx + 3] = a;
}
}
const resized = new PNG({ width, height });
PNG.bitblt(source, resized, 0, 0, width, height, 0, 0);
return resized;
};

四、插件化扩展总结

实际编写测试用例时遇到的问题,框架会考虑后续架构规划、开闭原则等方面,不一定立即就支持业务上的需求。而我们可以基于Tiga插件化的架构,快速将常用的功能扩展为1个新的模版:

模板地址:https://git.jd.com/chenlongde/tiga-plugin-http-mock-screenshot

基于Tiga跨端能力和插件化能力,我们解决了开头提到的两个问题:

  1. H5和小程序,维护一套测试用例即可
  2. 通过screenshot截图对比,用最轻量的方式编写测试用例

基于Tiga的项目实战

用例编写

背景:拼购详情页商品楼层利益点布局,目前已经存在4种:

  1. 新人价
  2. 补贴价
  3. 排行和N天最低价利益点
  4. N人已拼

业务上组合排列效果如下:

以只有新人价的场景为例,编写如下测试用例:

// 准备mock数据
const newerPrice = overrideMockFloor(MAIN_MOCK.guest_ing, 'fSkuInfo', (fData) => {
Object.assign(fData.skuInfo.skuPrice, {
"newerPrice": "¥8.9",
"newerPriceTag": "新人价",
"subsidyTag": "",
"subsidyTips": ""
})
Object.assign(fData.skuInfo, {
skuRankTxt: "",
nDayLowestPrice: ""
})
return fData
})
// 用例
const specs = [
{
name: '1. [渲染]新人价',
mock: [
{
test: API_REG.main,
data: {
status: 200,
body: newerPrice,
},
},
],
waiting: 1000,
screenshot: true,
},
// ...
];
// 配置
const config = {
env,
pageUrl,
specs,
testPath: path.resolve(__dirname, '../../pingou_detail'),
currentTestName: 'sku',
}
if (env === 'web') {
Object.assign(config, runtimeConfig.web)
}
// 执行
tiga.template().plugin(HttpMockRequestPlugin).config(config).exec()

需求迭代

在已经有上述测试用例的情况下,此时业务来了需求,加入1种新的利益点,如下图:

1. 前端改动flex布局的代码

修改justify-content、flex-wrap等css属性,并发到到 dev 开发机上

2. 开发环境自动化验证

代理到 dev 环境运行上述用例,确保这次改动不会影响到已有的各种布局

3. 为新特性编写用例

增加新特性的测试用例,持续进行迭代

以下为增加新的用例后的执行结果:

本地项目

对于一个自动化测试的项目,从长期维护、持续完善的角度看,最好的方式肯定是上云,并持久化数据进行进行质量跟踪。

目前 TigaV2 已经有上云和集成到CI的计划,本着不重复造轮子的思想,我们先在本地创建项目运行:

  • 基于 inquirer 做交互式命令行,支持用户选择不同环境、业务、特性做测试
  • 基于 child_process 分发不同的用力到子线程运行,监听结果集中到主线程展示
npm run test
? 请选择执行环境: H5
? 请选择测试业务: 拼购详情
? 请选择测试业务的特性: ALL
开始用例执行
canvas 6 passed, 6 total (34.626 s)
fx_banner 2 passed, 2 total (12.91 s)
tuan 2 failed, 1 passed, 3 total (48.972 s)
0. 普通商品Canvas图 (7.635 s)
1. 普通返现Canvas图 (5.223 s)
3. 百亿补贴Canvas图 (4.79 s)
4. 1分新人Canvas图 (4.655 s)
4. 1元新人Canvas图 (4.875 s)
5. 全额返Canvas图 (5.065 s)
5. 单默认pin返现 (7.337 s)
6. 双默认pin返现 (3.734 s)
1. 拼购店商品,点击参团,弹窗确认到结算 (17.976 s)
2. 自营商品,有延保,点击参团,弹窗确认到结算 (14.523 s)
3. 自营商品,无延保,点击参团,直接到结算 (14.087 s)

项目地址

可以参考拼购详情的自动化项目:https://git.jd.com/sjds/automator

最后

感谢Tiga团队带来的跨端自动化测试框架,从Taro到Tiga,希望跨端的技术研发体验越来越好~

Tiga官方咚咚群: 85652139

Tiga官网:http://bee.jd.com/d/tiga-doc/

TigaV2 RFC:https://git.jd.com/tiga/rfcs