前端动化测试的难点#
不同于API的自动化测试,Web/小程序前端自动化测试面临以下问题:
- H5和小程序运行时不一致,需要维护两套代码
- 页面元素多,没有精力去维护各个组件渲染的测试用例
- 不同系统不同版本兼容性不一致
- 移动设备种类多,成千上万种屏幕尺寸&分辨率
跨端自动化测试框架 —— Tiga#
大致原理见下图,基于puppeteer和automator,将两端的API抹平,同时提供辅助的AnyProxy代理等工具,详情见官方文档:
http://bee.jd.com/d/tiga-doc/
Tiga大致原理见下图:

基于Tiga扩展插件#
Template是Tiga作为开放式框架的重点支点,基于Template,我们可以将用户常用测试场景,抽象出一套通用测试逻辑,免去重复书写的繁琐
一、模版化的优势#
1. 更轻量#
去除了对Jest的强依赖,断言可以使用任意(BDD/TDD风格)的断言库,例如 jest.expect,chai.should
- BDD:Behavior Driven Development(expect/should)
行为驱动开发,是一种敏捷软件开发的技术,它鼓励软件项目中的开发,QA和非技术人员之间的协作。主要是以开发特性为视角,更适合开发人员进行单测等。
expect(props.length).toBe(1)
- TDD:Test-Driven Development(Assert)
测试驱动开发,是一种测试先于编写代码的思想用于指导软件开发,测试驱动开发是敏捷开发中的一项核心时间和技术,也是一种设计方法论,TDD的思想是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。
assert.lengthOf(props.length, 3, 'foo`s value has a length of 3');
2. 开放性/扩展性#
基于Tapable的插件模式诞生于webpack插件化,Tiga借鉴了Tapable插件化的思路,用户可以更方便地进行扩展,兼容更多场景。暴露的hooks如下:

二、实际编写用例时遇到的问题#
问题1: 首次http请求没有拦截#
addHttpRule逻辑是先访问页面,再注入httpRule规则
- 生成page,page访问对应页面
- page中注入httpRule
- 当前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) {
store.page = await store.app._parent.newPage()
}
async beforeEachSpec(spec, store) {
this.ruleIds = await store.page.addHttpRules(rules)
await store.page._parent.goto(spec.pageUrl || store.global._pageUrl)
}
2. 支持JSONP#
读取请求头的url中的callback参数,在数据处理时拼接字符串即可
static async onRequest(req) {
const jsonpCallback = getJSONPCallback(req.url())
const formatRes = (res) => {
try {
if (Object.prototype.toString.call(body) === '[object Object]') {
body = JSON.stringify(body)
if (jsonpCallback) {
body = `${jsonpCallback}(${body})`
}
}
} catch (e) {
}
return Object.assign({}, res, {
body,
})
}
}
3. 图片对比处理支持缩放#
视网膜屏是750px的图,普通屏幕是375px的图,因此从准确性看,需要将750px的图片数据缩放成375px版本的数据,然后再进行对比
通过阅读源码我们发现:pngjs处理后的data是一个 number[],每4个值代表1个点
因此可以通过放大比例,取到放大区域的四个顶点,将四个顶点的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++) {
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
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跨端能力和插件化能力,我们解决了开头提到的两个问题:
- H5和小程序,维护一套测试用例即可
- 通过screenshot截图对比,用最轻量的方式编写测试用例
基于Tiga的项目实战#
用例编写#
背景:拼购详情页商品楼层利益点布局,目前已经存在4种:
- 新人价
- 补贴价
- 排行和N天最低价利益点
- N人已拼
业务上组合排列效果如下:

以只有新人价的场景为例,编写如下测试用例:
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