最近几个月来,笔者一直在探索前端 UI 自动化测试的场景和方案。最初的时候,面对众多的技术选型,显得有些茫然,而团队此前也没有太多关于这方面的经验,只能一步一步摸索总结,当然期间也踩过不少坑,最终形成了一套相对稳定的测试方案,未来还将继续扩展和完善。
项目地址:jest-puppeteer-testing
在这篇文章中,笔者想和大家分享一下自己对于 UI 自动化测试的思考和经验。
为什么要进行 UI 自动化测试
业务的更新迭代频繁,传统测试大部分都还是手工、肉眼的模式来进行,无法满足产品敏捷开发、快速迭代的需求。而 UI 自动化能让全功能的回归变得简单,释放纯手工测试的人力资源,并且回归测试能够覆盖到所有的逻辑场景,这对测试的效率,以及整个开发流程的效率都是很大的提升,并且能够规避很多人的主观和客观因素导致的漏测或者疏忽。
其他测试方式的局限性:
单元测试(Unit Testing)
事实上,单元测试确实能够帮助我们发现大部分的问题,但是在复杂的前端交互中,单纯的单元测试并不能真实地反映用户操作的路径,而单元测试一般的场景是测试一系列的功能集合。
快照测试(Snapshot Testing)
DOM 结构并不能完全反映页面的视觉效果,DOM 结构不变并不完全等于样式不变。此外,大多数工具都是 React 专用,非 React 应用基本不支持。
笔者想说:
很多人认为,UI 总是频繁的变动,导致测试用例维护成本高,性价比低,因此 UI 自动化测试比较适合场景稳定的业务。其实不是,这里的 UI 不仅仅指的是视觉,更多的是业务逻辑。UI 可以多变,但业务逻辑一定是趋于稳定的,尤其是核心业务,想一想用户得多辛苦才能适应这种业务逻辑频繁变更的产品啊。
关于技术选型
TypeScript + Jest + Puppeteer
事实上,对于 UI 自动化测试来说,许多框架之间并没有太多差别,也从来不是影响整套测试用例是否健壮的关键性因素。相比之下,如何提高测试用例稳定性及全面性才是让 UI 自动化测试方案落地的重要细节。
开发实践
项目搭建
大家可以参考 jest-puppeteer-testing,这里不再累述。
核心文件
// setup/expect-image-snapshot.ts
// 让 jest 支持保存 / 比对屏幕截图
import {configureToMatchImageSnapshot} from 'jest-image-snapshot';
expect.extend({
toMatchImageSnapshot: configureToMatchImageSnapshot({customSnapshotsDir: '__image_snapshots__',}),
});
// setup/enhance-puppeteer.ts
// 增强 puppeteer 功能,如:拦截请求并使用 mock 数据
import {onRequestInterceptor} from '../utils/request';
jest.setTimeout(30000);
beforeAll(async () => {page.on('request', onRequestInterceptor); // 拦截请求,使用代理数据
await page.setRequestInterception(true);
});
// utils/request.ts
// mock 数据的核心文件
// 这里只拦截 xhr 或 fetch 请求,当然你也可以自行扩展
import {URL} from 'url';
import {Request} from 'puppeteer';
import mocks from '../mocks';
// 设置请求拦截器的数据,用于同一请求返回不同结果,生效一次后自动销毁
export const interceptors: {[api: string]: any } = {};
export const setRequestInterceptor = (api: string, value: any) => {interceptors[api] = value;
};
export const onRequestInterceptor = (request: Request) => {const resourceType = request.resourceType();
if (resourceType === 'xhr' || resourceType === 'fetch') {const location = new URL(request.url());
const mockKey = location.pathname;
if (mockKey && mocks.hasOwnProperty(mockKey)) {const mock = mocks[mockKey];
let response: any;
if (typeof mock === 'function') {response = mock({ location, request, interceptor: interceptors[mockKey] });
delete interceptors[mockKey]; // 生效一次后自动销毁
} else {response = mock;}
if (response) {if (response.body != null && typeof response.body === 'object') {response.body = JSON.stringify(response.body);
}
request.respond(response);
}
} else {request.continue();
}
} else {request.continue();
}
};
其他文件
-
shared.d.ts
定义数据类型 -
cases
目录下存放测试用例 -
mocks
目录下存放 mock 数据 -
utils
目录下存放工具方法
补充说明:关于 mock 的类型定义,可以在 shared.d.ts 中找到,当然你也可以在这里增加其他类型定义
经验总结
测试地址的选择(本地 / 线上)
- 本地服务器:请求响应快,测试结果稳定,但无法排除由线上环境差异或代码打包过程中引发的问题
- 线上服务器:能够反映网站真实的展示,无需额外启动服务器,任何时候都可以测试,但受网络因素影响,可能导致测试结果不稳定
尽量抹平不确定因素带来的影响
如维持数据请求的结果稳定,日期时间稳定,保证页面渲染的一致性。假如由于数据返回或时间的不确定性,导致每次页面渲染不一样,那这样测试也失去了意义。
尽量明确保存屏幕截图的时机
如访问一个页面后截图,由于网络因素的原因,图片资源并不是每次都加载完成,从而导致截图前后不一样。
……(暂时写这么多,有空再更)
总结
事实上,这套 UI 自动化测试方案更像是端到端测试(E2E Testing),即模拟一个用户将程序作为一个完全的黑盒,打开应用程序模拟输入,检查功能以及界面是否正确,配合屏幕截图可以直观感受到用户进行某些交互产生的具像化视觉效果。
项目地址:jest-puppeteer-testing