UI自动化测试框架 ---TestCafe

因某个机会轻轻碰了一下TestCafe,感受到其强大,这里写个文档,做个小结。

什么是TestCafe

官方:

A node.js tool to automate
end-to-end web testing
Write tests in JS or TypeScript, run them and view results

抓几个重点词语:1. E2E Web Testing 2.JS\TypeScript 3. Node.js Tool。
简单说就是Node.JS编写的Web端UI自动化测试框架。
官网:http://devexpress.github.io/testcafe/

TestCafe VS Selenium

这时我想你跟我都有个疑问,跟Selenium 有啥区别?这里个人简单阅读了下官方文档,写几个粗浅的对比。

对比项 Selenium 3.x TestCafe 谁更优
社区 Web自动化测试一哥 ,学习资料非常多 较新的工具,官方提供了详细的学习资料,Github上Start人数也超过5K Selenium
支持语言 java,python,ruby,Node.js...... JavaScript,TypeScript Selenium
支持的浏览器 Chrome,IE,Firefox,Safari,Edge等有Driver的浏览器都支持 支持所有能支持JS的浏览器,也就意味着支持多端 TestCafe
完善性 需要跟其它框架结合使用,如TestNG等 自身就是一个完整的自动化测试框架 TestCafe
易学性 简单易学 简单易学 不分上下

Selenium毕竟已经是Web自动化测试的W3C标准了,它有非常多的优势,但TestCafe 作为后起之秀我这还想夸夸Demo使用过程的几点优于Selenium的感受。

  • TestCafe 不再像Selenium 通过各个浏览器提供的Driver来驱动浏览器,而是有点类似Selenium RC直接往页面注入JS来操作页面,所以使用过程中再也不用担心因为浏览器版本和Driver版本以及Selenium版本不匹配而照成的Case执行失败。
  • TestCafe 是一整套完整的自动化测试框架,不仅提供了Cases的管理,运行,失败自动重跑,错误自动截图,并发等,对页面和页面元素的等待也封装完善而且使用简单,不像Selenium需要借助其他框架或者二次封装智能等待或者使用隐示/显示等待而有点复杂。
  • TestCafe 可以控制整体的执行速度,甚至可以细到某个操作的执行速度(这个有点类似慢放,用起来大家可以感受下,非常魔性)
  • TestCafe 因为只要你的浏览器支持JS,所以支持桌面,移动端平台。
  • TestCafe debug模式,通过代码配置或运行时设置,可以控制执行过程中失败时进入调试模式。

PS:当然以上的感受并没有经过项目的积累,纯粹Demo过程中的总结,也不晓得真正用到项目中会有哪些坑得踩。

TestCafe 快速入门

安装

因为是Node.js 项目,可以直接通过npm安装,全局安装如下

npm install -g testcafe

快速Demo一个

baidu.js

fixture `baidu demo`
    .page `https://www.baidu.com`;

test('baidu search', async t=>{
   await t.typeText('#kw',"hao123")
       .click('#su')
});

通过Chrome运行

testcafe chrome baidu.js

上面代码看不懂没关系,感受下TestCafe就行。

Cases管理

自动化测试,终归还是测试,是测试就离不开测试用例,那TestCafe如何组织管理测试用例?

fixture 和 test

一个js文件可以包含多个fixture,一个fixture可以包含多个test。 我们可以理解为fixture是个集合,test标注的每个函数模块是一个case。

语法

fixture("测试集描述")
fixture `测试集合描述`
test('用例描述',fn(t))

Demo

fixture("cases manage").page("https://www.baidu.com");

test('this case 1', async I => {
    console.log("this is case 1");
});
test('this case 2', async I => {
    console.log("this is case 2");
});
test('this case 3', async I => {
    console.log("this is case 3");
});


fixture(`cases manage 2`).page(`https://testerhome.com/#gsc.tab=0`);

test('this case 1-1', async I => {
    console.log("this is case 1-1");
});
test('this case 2-1', async I => {
    console.log("this is case 2-1");
});
test('this case 3-1', async I => {
    console.log("this is case 3-1");
});

运行结果:


image.png

其中你会发现每个test 执行之前都会执行fixture打开页面的操作,但只会启动一次浏览器。那这时又会一个新的问题,除了打开页面的前提条件,是否框架自带了更多的前提/后置条件的处理了,也就是各种beforexxx。
当然!

fixture 的前置条件

fixture.beforeEach( fn(t) ):每个test执行之前都会被运行
fixture.afterEach( fn(t) ):每个test执行之后都会被运行
fixture.before(fn(t)):比beforeEach更早运行,且每个fixture只运行一次
fixture.after(fn(t)):比afterEach更晚运行,且每个fixture只运行一次

Demoj


fixture(`beforeeach test1`)
    .page(`https://www.baidu.com`)
    .beforeEach(async I => {
        console.log('this is beforeEach')
    })
    .before(async I => {
        console.log('this is before')
    })
    .after(async I => {
        console.log('this is after')
    })
    .afterEach(async I=>{
        console.log("this is afterEach")
    });

test("test beforeAndafter",I=>{
    console.log("1111")
});

test("test beforeAndafter",I=>{
    console.log("2222")
});

运行结果:


image.png

test的前置条件

test.before(fun(t)):该test运行之前运行
test.after(fun(t)):该test运行之后运行

Demo

fixture(`beforeeach test1`)
    .page(`https://www.baidu.com`)
    .beforeEach(async I => {
        console.log('this is beforeEach')
    })
    .before(async I => {
        console.log('this is before')
    })
    .after(async I => {
        console.log('this is after')
    })
    .afterEach(async I => {
        console.log("this is afterEach")
    });

test
    .before(async t => {
        console.log(`this is test's before`)
    })
    ("test beforeAndafter", I => {
        console.log("1111")
    })
    .after(async t => {
        console.log(`this is test's after`)
    });

test("test beforeAndafter", I => {
    console.log("2222")
});

运行结果:


image.png

注意: 从控制台输出看,test的before/after 会覆盖fixture中的beforeEach/afterEach。也就是说如果一个test里面包含了before/after 那么fixture中的beforeEach/afterEach对该test无效。

跳过测试

fixture.skip :跳过该fixture下的所有test
test.skip : 跳过该test
fixture.only :只执行该fixture下的所有test,其余的fixture下的test全部跳过
test.only : 只运行该test,其余全部跳过

元素定位

Demo

  1. 创建Selectors
import { Selector } from 'testcafe';
  1. 使用Selectors
    // 通过css定位
    const osCount   = Selector('.column.col-2 label').count;
    // 通过id定位
    const submitButtonExists = Selector('#submit-button').exists;

同时因为是JS注入方式,所以定位方式非常灵活,几乎JS中定位元素的方式都支持。 例如

import { Selector } from 'testcafe';

fixture `My fixture`
    .page `http://devexpress.github.io/testcafe/example/`;

const label = Selector('#tried-section').child('label');

test('My Test', async t => {
    const labelSnapshot = await label();

    await t.click(labelSnapshot);
});

test('My test', async t => {
    const secondCheckBox = Selector('input')
        .withAttribute('type', 'checkbox')
        .nth(1);

    const checkedInputs = Selector('input')
        .withAttribute('type', 'checkbox')
        .filter(node => node.checked);

    const windowsLabel = Selector('label')
        .withText('Windows');

    await t
        .click(secondCheckBox)
        .expect(checkedInputs.count).eql(1)
        .click(windowsLabel);
});

同时还支持自定义扩展选择器,而且针对当前流行的React,Vue,Angular,Aurelia前端框架,还有特点的定位选择器,这里内容很多,有兴趣直接看官方文档:
http://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/

操作

元素操作其实上面例子我们已经用过点击,文本输入等方法了,官方也给了很全的api文档和demo:http://devexpress.github.io/testcafe/documentation/test-api/actions/ ,这里就讲下一些比较特殊的元素或浏览器的操作。

  • resizeWindow():设置窗口大小
  • t.maximizeWindow( ):最大化窗口
fixture`demo`.page('https://www.baidu.com');

test('设置win窗口大小', async I => {
    await I.resizeWindow(300, 500)
              ..maximizeWindow( );
});
  • getBrowserConsoleMessages():获取页面控制台消息
test('获取控制台输出', async I => {
    console.log(await I.getBrowserConsoleMessages())
});
  • wait():暂停
test('暂停', async I => {
    await I.wait(3000);
});

  • switchToIframe():切换到iframe
  • switchToMainWindow():返回到主窗体
fixture`iframe 处理 `
    .page`http://www.w3school.com.cn/tiy/t.asp?f=jseg_alert`;

test('iframe ', async t => {
    await t
        .click('#button-in-main-window')
        // 切换ifrme
        .switchToIframe('#iframe-1')
        // 返回主窗体
        .switchToMainWindow();
});
  • 下拉框选取:其实就是定位下拉框,再定位到下拉框下的选项,然后点击两次。
fixture`下拉框选取 `
    .page`file:///C:/Note/selenium_html/index.html`;

test.only('下拉框选取 ', async t => {
    const phone = Selector('#moreSelect');
    const phoneOption = phone.find('option');
    await t
        .click(phone)
        .click(phoneOption.withText('oppe'));
});
  • 三种警告框的处理setNativeDialogHandler(fn(type, text, url) [, options]):fu返回true 点击确定,返回false点击取消,返回文本则在prompt输入文本,这个执行过程中就不会看到警告框弹出,直接处理掉。
fixture`警告框处理 `
    .page`http://www.w3school.com.cn/tiy/t.asp?f=jseg_alert`;

test('处理alert ', async t => {
    await t
        .switchToIframe("iframe[name='i']")
        // return true 表示点击确定 
        .setNativeDialogHandler(() => true)
        .click('input[value="显示警告框"]')
        .wait(10000);
});

fixture`警告框处理 `
    .page`http://www.w3school.com.cn/tiy/t.asp?f=jseg_prompt`;

test.only('处理 提示框 ', async t => {
    await t
        .switchToIframe("iframe[name='i']")
        .setNativeDialogHandler((type, text, url) => {
            switch (type) {
                case 'confirm':
                    switch (text) {
                        //false 点击 取消
                        case 'Press a button!':
                            return false;
                        //    返回 true 点击确定
                        case 'You pressed Cancel!':
                            return true;
                        default:
                            throw 'Unexpected confirm dialog!';
                    }
                case 'prompt':
                    // 警告框填入值 hi vidor
                    return 'Hi vidor';
                case 'alert':
                    throw '我是警告框!!';
            }
        })
        .click('input[value="显示提示框"]')
        .wait(10000);
});
  • 上传文件setFilesToUpload(),清空上传:clearUpload():
fixture`My fixture`
    .page`http://www.example.com/`;

test('上传图片', async t => {
    await t
        .setFilesToUpload('#upload-input', [
            './uploads/1.jpg',
            './uploads/2.jpg',
            './uploads/3.jpg'
        ])
        // 清除上传
        .clearUpload('#upload-input')
        .click('#upload-button');
});

断言

TestCafe自带了较为齐全的断言方法。断言都是通过expect()开始;

import { Selector } from 'testcafe';

fixture `My fixture`;

test('My test', async t => {
    // 断言 通过CSS定位到的有3个元素,eql()表示相等,count表示定位元素个数
    await t.expect(Selector('.className').count).eql(3);
});

test('My test', async t => {
// 断言ok()表示为true,exists表示元素是否存在
    await t.expect(Selector('#element').exists).ok();
});

更多APIdemo查看官方文档:http://devexpress.github.io/testcafe/documentation/test-api/assertions/

特性

在介绍几个TestCafe比较有意思的几个地方。

执行速度

testcafe 支持测试执行的速度控制。 speed(x),x支持0.01到1之间,1则表示正常速度执行。

全局速度控制

可以通过控制台执行命令控制:
testcafe chrome xxxx.js --speed 0.1

控制某个test的执行速度

test("test setTestSpeed", I => {
    I.setTestSpeed(0.1);
    ......
});

控制某个步骤的执行速度

test("test setTestSpeed", I => {
    I.click("#kw").setTestSpeed(0.5);
});

Chrome设备模拟

在启动Chrome浏览器时,可以设定Chrome提供的模拟器。


image.png
testcafe "chrome:emulation:device=iphone x" xxx.js

设备模拟器更多参数查看:http://devexpress.github.io/testcafe/documentation/using-testcafe/common-concepts/browsers/using-chrome-device-emulation.html

PageObject demo

大家都知道,做UI自动化测试,肯定得使用PO或者PF模式,下面简单Demo个例子看看TestCafe 可以如何组织PO模式。
baiduPage.js

import {Selector, t as I} from 'testcafe'

class baiduPage {

    baiduInput = Selector('#kw');
    baiduButton = Selector('#su').withAttribute('value', '百度一下');

    async searchBaidu(text) {
        await I
            .typeText(this.baiduInput, text, {
                // 清空
                replace: true,
            })
            .click(this.baiduButton)
    };
}
export default baiduPage = new baiduPage();

baiduCases.js

import baiduPage from './baidu_page'


fixture`baidu search`.page`https://www.baidu.com/`;

test('po demo', async I => {

    await I.typeText(baiduPage.baiduInput, "test");
    
    baiduPage.searchBaidu("testCafe");
    
    await  I.typeText(baiduPage.baiduInput,"居于之前的字符串空两个字符中插入",{
        caretPos:2
    })
});

参数化/数据驱动

其实就是创建一个对象,用for ... of ... 循环遍历

fixture`todoPage test cases`.page`http://todomvc.com/examples/react/#/`;
const testCases = [
    {
        todo: '123',
    },
    {
        todo: '!@#$',
    }
    // 等等可能性的cases,这里随便造两个作为data driver
];

for (const todoText of testCases) {
    test('create todo list ' + todoText.todo, async t => {
        await todoPage.createTodoList(todoText.todo);
        await t.expect(todoPage.firstTodo.innerText).eql(todoText.todo);
    });
}

运行方式Runner

TestCafe 可以通过命令行的方式来执行测试脚本,但是感觉实际过程中肯定不是很方便,特别如果运行时需要跟一堆参数的情况下,那么TestCafe 提供了Runner,更方便配置和运行。
如下配置,我需要被运行的Cases,错误自动截图,并发,生成report,智能等待,执行速度,执行的浏览器等全部配到Runner里面,这样我就不需要通过命令行运行,而且在项目中使用非常方便。

const createTestCase = require('testcafe');
const fs = require('fs');

let testcafe = null;

createTestCase('localhost', 1337, 1338)
    .then(tc => {
        testcafe = tc;
        const runner = testcafe.createRunner();
        const stream = fs.createWriteStream('report.json');
        return runner
            // 需要运行的cases
            .src(
                [
                    '../demo/podemo/*.js',
                    '../demo/setWindowsSize.js'
                ]
            )
            // 设置需要执行的浏览器
            .browsers([
                'chrome',
                'firefox'
            ])
            // 错误自动截图
            .screenshots(
                // 保存路径
                '../error/',
                true,
                // 保存路劲格式
                '${DATE}_${TIME}/test-${TEST_INDEX}/${USERAGENT}/${FILE_INDEX}.png'
            )
            // 生成report格式,根据需要安装对应report模块,
            // 详细看:http://devexpress.github.io/testcafe/documentation/using-testcafe/common-concepts/reporters.html
            .reporter('json', stream)
            // 并发
            .concurrency(3)
            .run({
                skipJsErrors: true, // 页面js错误是否忽略,建议为true
                quarantineMode: true, // 隔离模式,可以理解为失败重跑
                selectorTimeout: 15000, // 设置页面元素查找超时时间,智能等待
                assertionTimeout: 7000, // 设置断言超时时间
                pageLoadTimeout: 30000, // 设置页面加载超时时间
                debugOnFail: true, // 失败开启调试模式 脚本编写建议开启
                speed: 1 // 执行速度0.01 - 1
            });
    }).then(failedCount => {
    console.error('Failed Count:' + failedCount);
    testcafe.close();
})
    .catch(err => {
        console.error(err);
    });

写在最后

TestCafe 还有非常多有意思的东西可以去发掘,例如跟Jenkins等集成一类的。 个人demo了一些例子觉得是个非常值得推荐的 UI 自动化测试框架,特别是用JS编写的在codecept,WebdriverIO我推荐TestCafe。 也许国内现在用的人不多,但相信是金子总会发光的。

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

推荐阅读更多精彩内容