TestCafe前端E2E自動(dòng)化測(cè)試技術(shù)要點(diǎn)
最近用TestCafe完成了一個(gè)營(yíng)銷活動(dòng)的前端自動(dòng)化測(cè)試眉撵,整個(gè)過程很順利苍日,運(yùn)行也較穩(wěn)定。對(duì)比以前用Selenium作的幾個(gè)Web UI自動(dòng)化項(xiàng)目而言蒲肋,感覺到了新一代的前端E2E自動(dòng)化測(cè)試工具的強(qiáng)大。下面記錄一些遇到的要點(diǎn)和TestCafe獨(dú)有的一些特性钝满。
結(jié)構(gòu)
TestCafe是個(gè)基于代理的自動(dòng)化測(cè)試工具兜粘,下面是它在測(cè)試運(yùn)行過程中所處的位置:
![image.png](https://upload-images.jianshu.io/upload_images/10532024-f80c1a7b741b070f.png&originHeight=276&originWidth=1776&search=&size=35320&status=done&width=888?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
正是因?yàn)檫@種結(jié)構(gòu),TestCafe多了一些特有的功能弯蚜。
Docker Image
TestCafe有Docker Image孔轴,可以在遠(yuǎn)程Linux主機(jī)終端下用chrome或firefox的headless方式運(yùn)行。
docker run -v /etc/localtime:/etc/localtime:ro -v `pwd`:/tests -it testcafe/testcafe chromium /tests/*.js
因?yàn)闇y(cè)試腳本中有獲取當(dāng)前時(shí)間的函數(shù)碎捺,導(dǎo)致時(shí)間對(duì)比時(shí)出錯(cuò)路鹰,是因?yàn)槿萜髦袝r(shí)區(qū)和宿主機(jī)不一致造成的。命令行加上-v /etc/localtime:/etc/localtime:ro
就解決了收厨。
HTTP Logging和Mock
被測(cè)系統(tǒng)中有一個(gè)銀行的營(yíng)銷活動(dòng)晋柱,是個(gè)轉(zhuǎn)盤抽獎(jiǎng),抽中何種獎(jiǎng)品是后端傳給前端的诵叁,前端的頁(yè)面中根據(jù)獎(jiǎng)品的不同有一些定制的邏輯雁竞。所以測(cè)試腳本需要提前獲取中了何種獎(jiǎng)品才能處理,不能僅僅作個(gè)Mock或配置成全部只中一個(gè)獎(jiǎng)品拧额,那樣覆蓋不到所有場(chǎng)景碑诉。好在TestCafe實(shí)質(zhì)是個(gè)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請(qǐng)求和響應(yīng)进栽,從resoonse中得到抽中獎(jiǎng)品
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);
...
然后就可以在測(cè)試代碼中根據(jù)抽中的獎(jiǎng)品作檢查漫仆、斷言判斷了。
Mock機(jī)制泪幌,很多現(xiàn)代的前端E2E自動(dòng)化測(cè)試工具比如Cypress也有盲厌,這方面沒什么好說的,看看官方文檔就可以用了祸泪。
Programming Interface
TestCafe提供了Programming Interface吗浩,可以用Node.js寫Running wrapper程序來跑測(cè)試,比如寫CLI或Web的Running Wrapper没隘,使得運(yùn)行測(cè)試的手段豐富了很多懂扼。
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中自動(dòng)注入Test Controller
UI自動(dòng)化測(cè)試中肯定是要用Page model的,TestCafe有個(gè)很好特性是會(huì)把Test Controller自動(dòng)注入到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中時(shí)阀湿,Test Controller將不能自動(dòng)注入到Selector中,可以在Selector中用{ boundTestRun: t }
選項(xiàng)瑰妄,并手工傳入Test Controller來實(shí)現(xiàn):
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元話費(fèi)')
.expect(actLuckUnit1.exists).ok()
...
.expect(actLuckUnit6.exists).ok()
.expect(actLuckUnit6.textContent).eql('神州專車')
...
.expect(actStartBtn.exists).ok()
.expect(actStartBtn.getAttribute('ng-click')).eql('showRecharge()');
...
}
這種自動(dòng)注入的特性陷嘴,在重用自動(dòng)化測(cè)試腳本方面很有用,你可以把公用的動(dòng)作放到Page model中间坐,也可以放到Helper方法中灾挨,不用手工傳遞Test Controller:
import { t } from 'testcafe';
...
export async inputGasNum(gasCardNo) {
await t
.typeText('#editboxGasNumber', gasCardNo)
...
}
然后在測(cè)試中引用此Helper,調(diào)用時(shí)也要加await竹宋。
豐富的Selector功能
查找界面元素
UI自動(dòng)化工具劳澄,首要的是選擇界面元素,TestCafe的Selector功能很豐富蜈七,建立Selector時(shí)參數(shù)支持:
- CSS選擇器
- 普通方法
- 其它的Selector或DOM Snapshot
- Promise
Selector還支持函數(shù)過濾秒拔,再次選擇,官網(wǎng)上的例子片段:
// 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)();
其實(shí)CSS3選擇器已經(jīng)足夠強(qiáng)大飒硅,平時(shí)使用時(shí)僅用它就可以滿足要求了砂缩。TestCafe還為常見的現(xiàn)代前端框架定制了Selcetor,這樣用組件就可以選擇界面元素:
- React
- Angular
- AngularJS
- Vue
- Aurelia
獲取界面元素屬性
可以直接調(diào)用Selector的方法獲取元素屬性或狀態(tài)狡相,也支持定制的Timeout:
await t
.expect(packagePage.packageItem.exists).ok( {timeout: PFOIL.pfoilAssertionDelayLevel3} )
.expect(packagePage.packageType.textContent).contains(labels.labelTwo);
還能通過addCustomDOMProperties
和addCustomMethods
擴(kuò)展Selector的屬性和方法梯轻。這樣的方法是運(yùn)行在瀏覽器端的食磕。用這樣的方法尽棕,所有DOM元件的屬性和方法,都可以從瀏覽器端獲取和執(zhí)行到彬伦。
要注意的有兩點(diǎn):
第一個(gè)是在代碼中使用Selector的屬性而不是在Assertion方法中時(shí)滔悉,需要手工加上awiat:
// 狀態(tài)變化有個(gè)時(shí)間差:先是鎖定伊诵,等支付成功回調(diào),才會(huì)變成已使用
const stateIsCorrect = (await packagePage.packageStateUsed.exists || await packagePage.packageStateLocked.exists);
await t.expect(stateIsCorrect).ok();
第二個(gè)是在Node.js代碼中使用Selector和Test controller時(shí)回官,需要用到boundTestRun
屬性曹宴,官網(wǎng)的例子:
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自動(dòng)化以前會(huì)面臨一個(gè)頭痛的問題,就是延時(shí)等待歉提。TestCafe會(huì)在斷言其間笛坦,不斷地去試探,直到超時(shí)才認(rèn)為是失敗苔巨。官網(wǎng)上的示意圖:
![image.png](https://upload-images.jianshu.io/upload_images/10532024-80cb2b853feb98fc.png&originHeight=298&originWidth=1368&search=&size=86243&status=done&width=684?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
在我實(shí)戰(zhàn)的過程版扩,只在幾個(gè)慢的節(jié)點(diǎn)增加了超時(shí)處理,延長(zhǎng)了缺省的內(nèi)置超時(shí)時(shí)間侄泽。而且TestCafe還能靈活地指定的Page loading時(shí)間礁芦。幾乎不需過多考慮顯式等待的情況。
只要是Client function或Selector傳入Assertion方法中悼尾,都會(huì)自動(dòng)觸發(fā)Smart Assertion Query機(jī)制柿扣。
Client Function
TestCafe實(shí)際上有兩部分,一部分是代理即TestCafe Server闺魏,一部分是在瀏覽器上運(yùn)行的所謂的Client部分未状。因?yàn)镃lient Function是和被測(cè)系統(tǒng)一起運(yùn)行在一個(gè)瀏覽器中的,這就有了很多的可能性析桥,比如娩践,獲取前面Selector機(jī)制不容易得到的界面元素,調(diào)用被測(cè)系統(tǒng)方法烹骨,實(shí)時(shí)計(jì)算和被測(cè)系統(tǒng)相關(guān)的東西等等翻伺。官網(wǎng)上有個(gè)例子:
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運(yùn)行時(shí),還可以把當(dāng)前測(cè)試代碼上下文中的一些變量或工具方法用注入到其內(nèi)沮焕,使用起來很方便:
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的特性非常豐富,也很容易學(xué)習(xí)峦树。用VS Code來寫TestCafe的測(cè)試腳本非常順暢辣辫。限于篇幅,其它一些主要特性就只列舉如下魁巩,可以閱讀官文文檔再實(shí)踐即可熟練掌握:
- 因?yàn)樗莏avascript急灭,所以用所有的現(xiàn)代瀏覽器來跑兼容性測(cè)試
- User Roles機(jī)制,抽象了用戶鑒權(quán)谷遂,用于處理有登陸的測(cè)試任務(wù)葬馋,支持cookie和browser storage
- Accessing Console Messages,可以訪問控制臺(tái)消息,在有些情況下可能很有用
- 暫停畴嘶、調(diào)試蛋逾,因?yàn)門estCafe Server是個(gè)Node程序,所以支持在VS code和在Chrome中調(diào)試
- Live mode模式窗悯,修改測(cè)試后区匣,自動(dòng)運(yùn)行
- 并行測(cè)試,每個(gè)運(yùn)行的瀏覽器都是隔離的
- 非常方便靈活蒋院,可自定義地處理Native Dialogbox
- 內(nèi)置了大量的等待機(jī)制亏钩,等待界面元素、動(dòng)作欺旧、斷言铸屉、HXR和Fetch請(qǐng)求、重定向等等
- 在夾具和測(cè)試上都支持測(cè)試框架通用的Setup切端、Teardown彻坛、Tag機(jī)制,只是和其它框架相比名稱不同而已
- 失敗后截屏踏枣、錄屏昌屉;用
quarantine-mode
支持失敗重跑 - 支持remote browser,可以讓測(cè)試跑在沒有安裝TestCafe和測(cè)試腳本的設(shè)備上茵瀑,比如可在手機(jī)上跑測(cè)試
- 不同的節(jié)點(diǎn)间驮、層級(jí)上都可以指定加載時(shí)間、運(yùn)行速度
- 運(yùn)行時(shí)具備命令行马昨、代碼竞帽、配置文件等不同層面的配置項(xiàng)
- 可以靈活地用不同方式地向被測(cè)應(yīng)用注入第三方模塊或代碼,然后用在測(cè)試代碼中
- 支持夾具或測(cè)試之間的上下文共享(其實(shí)也可用一個(gè)全局的自定義模塊來作為上下文)
- ......
TestCafe很強(qiáng)大鸿捧,也很易用屹篓,因?yàn)闇y(cè)試用例都是代碼,所以有強(qiáng)大的表現(xiàn)能力匙奴,表現(xiàn)力對(duì)于復(fù)雜的自動(dòng)化測(cè)試來說非常重要堆巧。TestCafe支持用現(xiàn)代的JavaScript(ES7)、TypeScript 和 CoffeeScript來寫測(cè)試泼菌。如有興趣谍肤,可以從官方文檔上學(xué)習(xí)更細(xì)節(jié)的內(nèi)容。