TestCafe前端E2E自動化測試技術要點
最近用TestCafe完成了一個營銷活動的前端自動化測試,整個過程很順利,運行也較穩(wěn)定。對比以前用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
因為測試腳本中有獲取當前時間的函數(shù),導致時間對比時出錯,是因為容器中時區(qū)和宿主機不一致造成的。命令行加上-v /etc/localtime:/etc/localtime:ro就解決了。
HTTP Logging和Mock
被測系統(tǒng)中有一個銀行的營銷活動,是個轉盤抽獎,抽中何種獎品是后端傳給前端的,前端的頁面中根據(jù)獎品的不同有一些定制的邏輯。所以測試腳本需要提前獲取中了何種獎品才能處理,不能僅僅作個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);
...
然后就可以在測試代碼中根據(jù)抽中的獎品作檢查、斷言判斷了。
Mock機制,很多現(xiàn)代的前端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來實現(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元話費')
.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時參數(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)();
其實CSS3選擇器已經(jīng)足夠強大,平時使用時僅用它就可以滿足要求了。TestCafe還為常見的現(xiàn)代前端框架定制了Selcetor,這樣用組件就可以選擇界面元素:
- React
- Angular
- AngularJS
- Vue
- Aurelia
獲取界面元素屬性
可以直接調用Selector的方法獲取元素屬性或狀態(tài),也支持定制的Timeout:
await t
.expect(packagePage.packageItem.exists).ok( {timeout: PFOIL.pfoilAssertionDelayLevel3} )
.expect(packagePage.packageType.textContent).contains(labels.labelTwo);
還能通過addCustomDOMProperties和addCustomMethods擴展Selector的屬性和方法。這樣的方法是運行在瀏覽器端的。用這樣的方法,所有DOM元件的屬性和方法,都可以從瀏覽器端獲取和執(zhí)行到。
要注意的有兩點:
第一個是在代碼中使用Selector的屬性而不是在Assertion方法中時,需要手工加上awiat:
// 狀態(tài)變化有個時間差:先是鎖定,等支付成功回調,才會變成已使用
const stateIsCorrect = (await packagePage.packageStateUsed.exists || await packagePage.packageStateLocked.exists);
await t.expect(stateIsCorrect).ok();
第二個是在Node.js代碼中使用Selector和Test controller時,需要用到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自動化以前會面臨一個頭痛的問題,就是延時等待。TestCafe會在斷言其間,不斷地去試探,直到超時才認為是失敗。官網(wǎng)上的示意圖:

在我實戰(zhàn)的過程,只在幾個慢的節(jié)點增加了超時處理,延長了缺省的內置超時時間。而且TestCafe還能靈活地指定的Page loading時間。幾乎不需過多考慮顯式等待的情況。
只要是Client function或Selector傳入Assertion方法中,都會自動觸發(fā)Smart Assertion Query機制。
Client Function
TestCafe實際上有兩部分,一部分是代理即TestCafe Server,一部分是在瀏覽器上運行的所謂的Client部分。因為Client Function是和被測系統(tǒng)一起運行在一個瀏覽器中的,這就有了很多的可能性,比如,獲取前面Selector機制不容易得到的界面元素,調用被測系統(tǒng)方法,實時計算和被測系統(tǒng)相關的東西等等。官網(wǎng)上有個例子:
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,所以用所有的現(xiàn)代瀏覽器來跑兼容性測試
- 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和測試腳本的設備上,比如可在手機上跑測試
- 不同的節(jié)點、層級上都可以指定加載時間、運行速度
- 運行時具備命令行、代碼、配置文件等不同層面的配置項
- 可以靈活地用不同方式地向被測應用注入第三方模塊或代碼,然后用在測試代碼中
- 支持夾具或測試之間的上下文共享(其實也可用一個全局的自定義模塊來作為上下文)
- ......
TestCafe很強大,也很易用,因為測試用例都是代碼,所以有強大的表現(xiàn)能力,表現(xiàn)力對于復雜的自動化測試來說非常重要。TestCafe支持用現(xiàn)代的JavaScript(ES7)、TypeScript 和 CoffeeScript來寫測試。如有興趣,可以從官方文檔上學習更細節(jié)的內容。