TestCafe前端E2E自动化测试技术要点
最近用TestCafe完成了一个营销活动的前端自动化测试,整个过程很顺利,运行也较稳定。对比以前用Selenium作的几个Web UI自动化项目而言,感觉到了新一代的前端E2E自动化测试工具的强大。下面记录一些遇到的要点和TestCafe独有的一些特性。
结构
TestCafe是个基于代理的自动化测试工具,下面是它在测试运行过程中所处的位置:
正是因为这种结构,TestCafe多了一些特有的功能。
Docker Image
TestCafe有Docker Image,可以在远程Linux主机终端下用chrome或firefox的headless方式运行。
docker run -v /etc/localtime:/etc/localtime:ro -v `pwd`:/tests -it testcafe/testcafe chromium /tests/*.js
因为测试脚本中有获取当前时间的函数,导致时间对比时出错,是因为容器中时区和宿主机不一致造成的。命令行加上-v /etc/localtime:/etc/localtime:ro
就解决了。
HTTP Logging和Mock
被测系统中有一个银行的营销活动,是个转盘抽奖,抽中何种奖品是后端传给前端的,前端的页面中根据奖品的不同有一些定制的逻辑。所以测试脚本需要提前获取中了何种奖品才能处理,不能仅仅作个Mock或配置成全部只中一个奖品,那样覆盖不到所有场景。好在TestCafe实质是个Http代理,所以它提供了HTTP logging的功能。
const turntableLogger = RequestLogger(/getTurntPrize/, {
// logRequestHeaders: true, logRequestBody: true,
logResponseHeaders: true, logResponseBody: true
})
...
test
.meta('testID', 'F004-T003')
.requestHooks(turntableLogger)
('xxx', async t => {
...
// 从logger中获取http请求和响应,从resoonse中得到抽中奖品
const httpLog = turntableLogger.requests[0];
await t.expect(turntableLogger.contains(log => log.response.statusCode === 200)).ok();
const resJSON = JSON.parse(httpLog.response.body.toString());
PFOIL.turntableCouponName = resJSON.data[0].voucherName;
const wonClz = actPage.getCouponClz(PFOIL.turntableCouponName);
...
然后就可以在测试代码中根据抽中的奖品作检查、断言判断了。
Mock机制,很多现代的前端E2E自动化测试工具比如Cypress也有,这方面没什么好说的,看看官方文档就可以用了。
Programming Interface
TestCafe提供了Programming Interface,可以用Node.js写Running wrapper程序来跑测试,比如写CLI或Web的Running Wrapper,使得运行测试的手段丰富了很多。
const createTestCafe = require('testcafe');
let testcafe = null;
createTestCafe('localhost', 1337, 1338)
.then(tc => {
testcafe = tc;
const runner = testcafe.createRunner();
return runner
.src('./*.js')
.browsers(['chrome:headless'])
.run({
skipJsErrors: true,
speed: 1,
quarantineMode: true,
takeScreenshotsOnFails: true,
stopOnFirstFail: false
});
})
.then(failedCount => {
console.warn('Tests failed: ' + failedCount);
testcafe.close();
})
.catch(err => { /* ... */ })
Page model中自动注入Test Controller
UI自动化测试中肯定是要用Page model的,TestCafe有个很好特性是会把Test Controller自动注入到Page model的方法中去。例如:
mport { Selector, t } from 'testcafe';
const PFOIL = require('../lib/context.js');
class ActivityTrunTablePage {
constructor() {
this.actLuckUnit0 = Selector('.pf-lottery-box .luck-unit-0');
...
async start() {
await t.click(this.actStartBtn);
}
...
但是想把断言部分也放到Page model中时,Test Controller将不能自动注入到Selector中,可以在Selector中用{ boundTestRun: t }
选项,并手工传入Test Controller来实现:
async validateTurntableElement(t) {
const actLuckUnit0 = Selector('.pf-lottery-box .luck-unit-0', { boundTestRun: t });
...
const actStartBtn = Selector('#btn', { boundTestRun: t });
const actDetail = Selector('.pf-lottery-detail', { boundTestRun: t });
await t
.expect(actLuckUnit0.exists).ok({ timeout: PFOIL.pfoilAssertionDelayLevel1 })
.expect(actLuckUnit0.textContent).eql('1元话费')
.expect(actLuckUnit1.exists).ok()
...
.expect(actLuckUnit6.exists).ok()
.expect(actLuckUnit6.textContent).eql('神州专车')
...
.expect(actStartBtn.exists).ok()
.expect(actStartBtn.getAttribute('ng-click')).eql('showRecharge()');
...
}
这种自动注入的特性,在重用自动化测试脚本方面很有用,你可以把公用的动作放到Page model中,也可以放到Helper方法中,不用手工传递Test Controller:
import { t } from 'testcafe';
...
export async inputGasNum(gasCardNo) {
await t
.typeText('#editboxGasNumber', gasCardNo)
...
}
然后在测试中引用此Helper,调用时也要加await。
丰富的Selector功能
查找界面元素
UI自动化工具,首要的是选择界面元素,TestCafe的Selector功能很丰富,建立Selector时参数支持:
- CSS选择器
- 普通方法
- 其它的Selector或DOM Snapshot
- Promise
Selector还支持函数过滤,再次选择,官网上的例子片段:
// Returns id of the third element in the set
const id = await Selector('ul').find('label').parent('div.someClass').nth(2).id;
// Returns snapshot for the fourth element in the set
const snapshot = await Selector('ul').find('label').parent('div.someClass').nth(4)();
其实CSS3选择器已经足够强大,平时使用时仅用它就可以满足要求了。TestCafe还为常见的现代前端框架定制了Selcetor,这样用组件就可以选择界面元素:
- React
- Angular
- AngularJS
- Vue
- Aurelia
获取界面元素属性
可以直接调用Selector的方法获取元素属性或状态,也支持定制的Timeout:
await t
.expect(packagePage.packageItem.exists).ok( {timeout: PFOIL.pfoilAssertionDelayLevel3} )
.expect(packagePage.packageType.textContent).contains(labels.labelTwo);
还能通过addCustomDOMProperties
和addCustomMethods
扩展Selector的属性和方法。这样的方法是运行在浏览器端的。用这样的方法,所有DOM元件的属性和方法,都可以从浏览器端获取和执行到。
要注意的有两点:
第一个是在代码中使用Selector的属性而不是在Assertion方法中时,需要手工加上awiat:
// 状态变化有个时间差:先是锁定,等支付成功回调,才会变成已使用
const stateIsCorrect = (await packagePage.packageStateUsed.exists || await packagePage.packageStateLocked.exists);
await t.expect(stateIsCorrect).ok();
第二个是在Node.js代码中使用Selector和Test controller时,需要用到boundTestRun
属性,官网的例子:
test('Title changed', async t => {
const boundSelector = elementWithId.with({ boundTestRun: t });
// Performs an HTTP request that changes the article title on the page.
// Resolves to a value indicating whether the title has been changed.
const match = await new Promise(resolve => {
const req = http.request(/* request options */, res => {
if(res.statusCode === 200) {
boundSelector('article-title').then(titleEl => {
resolve(titleEl.textContent === 'New title');
});
}
});
req.write(title)
req.end();
});
await t.expect(match).ok();
});
Smart Assertion Query
Web UI自动化以前会面临一个头痛的问题,就是延时等待。TestCafe会在断言其间,不断地去试探,直到超时才认为是失败。官网上的示意图:
在我实战的过程,只在几个慢的节点增加了超时处理,延长了缺省的内置超时时间。而且TestCafe还能灵活地指定的Page loading时间。几乎不需过多考虑显式等待的情况。
只要是Client function或Selector传入Assertion方法中,都会自动触发Smart Assertion Query机制。
Client Function
TestCafe实际上有两部分,一部分是代理即TestCafe Server,一部分是在浏览器上运行的所谓的Client部分。因为Client Function是和被测系统一起运行在一个浏览器中的,这就有了很多的可能性,比如,获取前面Selector机制不容易得到的界面元素,调用被测系统方法,实时计算和被测系统相关的东西等等。官网上有个例子:
import fs from 'fs';
import { ClientFunction } from 'testcafe';
...
const getDataFromClient = ClientFunction(() => getSomeData());
test('Check client data', async t => {
const boundGetDataFromClient = getDataFromClient.with({ boundTestRun: t });
const equal = await new Promise(resolve => {
fs.readFile('./data/clientData.json', (err, data) => {
boundGetDataFromClient().then(clientData => {
resolve(JSON.stringify(clientData) === data);
});
});
});
await t.expect(equal).ok();
});
在Client Function运行时,还可以把当前测试代码上下文中的一些变量或工具方法用注入到其内,使用起来很方便:
const thirdOption = option.nth(2);
const getThirdOptionHTML = ClientFunction(() => option().innerHTML, {
dependencies: { option: thirdOption }
});
...
const getDocumentURI = require('./utils.js').getDocumentURI;
...
test('My test', async t => {
const getUri = ClientFunction(() => {
return getDocumentURI();
}, { dependencies: { getDocumentURI } });
const uri = await getUri();
...
其它
一路用下来,感觉TestCafe的特性非常丰富,也很容易学习。用VS Code来写TestCafe的测试脚本非常顺畅。限于篇幅,其它一些主要特性就只列举如下,可以阅读官文文档再实践即可熟练掌握:
- 因为它是javascript,所以用所有的现代浏览器来跑兼容性测试
- User Roles机制,抽象了用户鉴权,用于处理有登陆的测试任务,支持cookie和browser storage
- Accessing Console Messages,可以访问控制台消息,在有些情况下可能很有用
- 暂停、调试,因为TestCafe Server是个Node程序,所以支持在VS code和在Chrome中调试
- Live mode模式,修改测试后,自动运行
- 并行测试,每个运行的浏览器都是隔离的
- 非常方便灵活,可自定义地处理Native Dialogbox
- 内置了大量的等待机制,等待界面元素、动作、断言、HXR和Fetch请求、重定向等等
- 在夹具和测试上都支持测试框架通用的Setup、Teardown、Tag机制,只是和其它框架相比名称不同而已
- 失败后截屏、录屏;用
quarantine-mode
支持失败重跑 - 支持remote browser,可以让测试跑在没有安装TestCafe和测试脚本的设备上,比如可在手机上跑测试
- 不同的节点、层级上都可以指定加载时间、运行速度
- 运行时具备命令行、代码、配置文件等不同层面的配置项
- 可以灵活地用不同方式地向被测应用注入第三方模块或代码,然后用在测试代码中
- 支持夹具或测试之间的上下文共享(其实也可用一个全局的自定义模块来作为上下文)
- ......
TestCafe很强大,也很易用,因为测试用例都是代码,所以有强大的表现能力,表现力对于复杂的自动化测试来说非常重要。TestCafe支持用现代的JavaScript(ES7)、TypeScript 和 CoffeeScript来写测试。如有兴趣,可以从官方文档上学习更细节的内容。