TestCafe前端E2E自动化测试技术要点

TestCafe前端E2E自动化测试技术要点

最近用TestCafe完成了一个营销活动的前端自动化测试,整个过程很顺利,运行也较稳定。对比以前用Selenium作的几个Web UI自动化项目而言,感觉到了新一代的前端E2E自动化测试工具的强大。下面记录一些遇到的要点和TestCafe独有的一些特性。

结构

TestCafe是个基于代理的自动化测试工具,下面是它在测试运行过程中所处的位置:


image.png
image.png

正是因为这种结构,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);

还能通过addCustomDOMPropertiesaddCustomMethods扩展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会在断言其间,不断地去试探,直到超时才认为是失败。官网上的示意图:


image.png
image.png

在我实战的过程,只在几个慢的节点增加了超时处理,延长了缺省的内置超时时间。而且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来写测试。如有兴趣,可以从官方文档上学习更细节的内容。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容