Web 自动化测试框架
基于 JS/TS 自动化测试框架构结构图
Selenium
定位元素
自动化测试要做的就是模拟鼠标和键盘操作这些元素,比如点击、悬停、键盘操作等。
而操作这些元素的前提是要定位它们。自动化工具 selenium 无法像测试人员一样通过肉眼来分辨页面上的元素。
那么浏览器的页面都是由 HTML 代码组成的,通过Chrom浏览器自带的开发者工具可以看到,它们之间有层级地组织起来,每个元素都有不同的标签名和属性值,WebDriver 就是根据这些信息来定位元素的。
WebDriver 提供了 8 种定位方式
-
id 定位
-
name 定位
-
tagName 定位
-
class 定位
-
linkText 定位
-
partialLinkText 定位
-
xpath 定位
-
css 定位
以上 8 种定位方式总结下来就分为两类,xpah 和 css 定位
定位一个元素
// WebDriver方法
loginEle = driver.findElement(By.css('a[cmd="toMyHomePage"] img[alt="Avatar"]'));
// 内部封装方法
loginEle = this.empty().css('a[cmd="toMyHomePage"] img[alt="Avatar"]')
// loginEle = PageElement(this.driver, By.css('a[cmd="toMyHomePage"] img[alt="Avatar"]'));
定义一组元素
WebDriver 定位一组元素与定位当个元素方法非常形似,唯一的区别就是“element” 后面多了一个 “s“,用来表示复数。
// WebDriver方法
loginEle = driver.findElements(By.css('a[cmd="toMyHomePage"] img[alt="Avatar"]'));
// 内部封装方法
loginEle = PageElements(this.driver, By.css('a[cmd="toMyHomePage"] img[alt="Avatar"]'));
页面操作
控制浏览器
WebDriver 主要提供操作页面上各种元素的方法,同时,它还提供了浏览器一些方法,如控制浏览器大小、操作浏览器前进或后退等。
常用的浏览器操作:
// 关闭浏览器
await driver.quit();
// 关闭当前窗口
driver.close();
// 浏览器窗口操作
await driver.manage().window().maximize();
await driver.manage().window().minimize();
await driver.manage().window().getRect();
await driver.manage().window().setRect({x: 50, y: 0, width: 800, height: 600});
// 控制浏览器前进后退
await driver.navigate().forward();
await driver.navigate().back();
// 模拟浏览器刷新
await driver.navigate().refresh();
WebDriver 中常用的方法
定位元素之后还需要对这个元素进行曹祖,比如单击(按钮)或输入(输入框)。
鼠标操作
hover():鼠标悬停
click():左击
doubleClick(): 双击
contextClick(): 右键
键盘操作
sendKeys() 方法可以用来模拟键盘输入,还可以用它来输入键盘上的组合键
示例:
// 发送单个字符
await driver.actions().sendKeys('keys');
// 模拟键盘快捷键 Ctrl + C
await driver.actions().sendKeys(Key.CONTROL, 'c');
多表单切换
iframe id跳转
ifram
等待
1. 强制等待
调用系统延时进行强制等待,此时系统不做任何操作。
示例:
driver.sleep(1000); // 等待 1s
2. 隐式等待
WebDriver 提供了隐式等待,用户相对比较简单,隐式等待的作用域为整个 driver。
示例:
driver.manage().setTimeouts( { implicit: 1000 }; // 全局等待 1s
wait 等待方法,在设置时间内, 每间隔一段时间检测一次当前页面是否存在,如果超过设置时间仍检测不到,则抛出异常。
3. 显示等待
显示等待是 WebDriver 等待某个条件成立则继续执行,否则在到达最大时长时抛出异常,显示等待作用域为语句。
示例:
await driver.wait(unitl.elementLocted(By.id("password"), 5000); // 显示等待 5s
wait 等待方法,在设置时间内, 每间隔一段时间检测一次当前页面是否存在,如果超过设置时间仍检测不到,则抛出异常。
wait 一般与 until() 方法配合使用。
Jest 单元测试框架
单元测试是一项技术要求很高的工作,只有白盒测试人员和软件开发人员才能胜任,单元测试框架做单元测试很简单,而且单元测试框架还适用与不同类型的 “自动化”测试。
- 提供测试用例组织和执行
在 typescript 种,我们编写代码可以定义类、方法和函数,那么如何定义一条“测试用例”? 如可灵活控制这些“测试用例”的执行?
jest 单元测试框架会告诉我们。 - 提供丰富的断言断言方法(匹配器)
当我们进行功能测试时,测试用例需要预期结果。当测试用例的执行结果与预期结果不一致时,就判定测试用例失败。在自动化测试种,通过“断言”来判定测试用例执行成功与否。 jest 测试框架种提供了丰富的断言库。例如:大于、小于、等于、不等于、包含等等。 - 自动导出测试报告
自动化测试在运行时并不需要人工干预,因此执行的结果非常重要。我们需要冲结果中清晰地看出失败的原因。另外,我们还需要统计测试用例执行的结果,如用例执行、成功、失败的用例数等,这些功能也是由自动化测试框架提供。
Jest 自动化测试框架
在 typescript 中有诸多单元测试框架,如 Mocha、Jasmine、Jest、Tape、Karma等,本项目使用 Jest 单元测试框架
使用 jest 单元测试框架具有以下特点:
- 速度快
- API 简单
jest的API很简单,API数量少,简单的学习就可以使用 - 易配置
在项目中安装jest,通过jest配置文件简单的配置就可以使用jest了 - 隔离性好
jest中会有很多测试文件,但每个测试文件去执行的时候,它的执行环境都是隔离的就会避免不同的测试文件执行的时候之间会产生互相影响。 - 监控模式
使用监控模式可以使得我们更加灵活的使用测试用例。 - IDE整合
jest可以很方便的与ide整合,也就是在编辑器中做前端自动化测试也会变得很简单 - Snapshot
- 多项目并行
- 测试覆盖率
- Mock丰富
Jest 自动化框架原理
认识单元测试框架
不用单元测试框架也能写单元测试,单元测试本质上就是通过一段代码去验证另外一段代码,所以不用单元测试框架也可以写单元测试。下面通过例子演示单元测试框架的原理。
- 不应用 jest 测试框架进行编写测试用例,使用顺序执行方式运行测试用例。
创建一个文件 imitationJest.ts
function testEqual(actual: any, expectVal: any) {
if (actual != expectVal) {
throw new Error(`实际值${actual}与预期值${expectVal}不相等`);
}
}
function testLessThan(actual: any, expectVal: any) {
if (actual > expectVal) {
throw new Error(`实际值${actual}大于预期值${expectVal}`);
}
}
function main() {
// 验证第一 12 是否等于15
testEqual(12, 15);
// 此用例不会被执行
// 验证第二个 12 是否小于 15
testLessThan(12, 15);
}
main();
$ ts-node test.ts
F:\文档\软件测试\Typescript\Typescript-Selenium-PageObject\imitationJest.ts:3
throw new Error(`实际值${actual}与预期值${expectVal}不相等`);
^
Error: 实际值12与预期值15不相等
at testEqual (F:\文档\软件测试\Typescript\Typescript-Selenium-PageObject\imitationJest.ts:3:11)
at main (F:\文档\软件测试\Typescript\Typescript-Selenium-PageObject\imitationJest.ts:15:3)
在测试代码中,在运行到 main 函数中的 testEqual 方法时,代码抛出异常,后面的 testLessThan() 测试用例函数不能执行,最后,执行结果无法统计。
当然,我们可以呕吐难过编写更多代码来解决这些问题,但是者就偏离了我们做单元测试的初衷。我们的应该将重点放在测试本身,而不是其它方面。下面就模拟 jest 测试框架,演示 jest 单元测试的原理。
创建一个文件 imitationJest.ts
// 模拟预期函数
// 断言机制
function myExpect(actual: any) {
return {
toEqual: function (expectVal: any) {
if(actual != expectVal) {
let err = `实际值${actual}与预期值${expectVal}不相等`;
console.log(err);
throw Error(err);
}
},
toBeLessThan: function (expectVal: any) {
if (actual > expectVal ) {
let err = `实际值${actual}大于预期值${expectVal}`;
console.log(err);
throw Error(err);
}
},
toBe: function() {
// pass
}
}
}
// 模拟 test 函数
// 单元测试机制
function myTest(desc: string, fn:Function) {
try {
fn();
console.log(`${desc} 通过测试`);
}catch(e) {
console.log(`${desc} 未通过测试`);
// fn() 运行失败抛出异常,不影响后面的测试用例
}
}
// 实战模拟 test 测试
myTest('第一个失败的测试用例,测试原理', () => {
console.log('第一个测试用例');
myExpect(12).toEqual(10);
});
// 实战模拟 test 测试
myTest('第二个测试用例', () => {
console.log('第二个测试用例');
myExpect(12).toBeLessThan(15);
});
imitationJest.ts 运行结果
$ ts-node imitationJest.ts
第一个测试用例
实际值12与预期值10不相等
第一个失败的测试用例,测试原理 未通过测试
第二个测试用例
第二个测试用例 通过测试
使用 类 jest 单元测试的原理,第一测试用例失败不会影响第二个测试用例,而测试结果也更加详细。
我们不需要写单元测试框架部分,jest 为我们提供了更多的单元测试函数。
Jest 中的钩子函数
引入 jest 单元测试框架。如果想使用 Jest 编写测试用例,那么一定要遵守它的“规则”。
- 创建一个测试套件, 这里为 describe, 测试套件是测试用例、测试套件或两者的集合,用于组装一组要运行的测试用例;
- 在测试套件内使用 test 函数, test 是最小的测试单元;
- 在 test 函数中编写测试用例,以及使用 expect 进行断言。
- 在测试套件中适当的使用 beforAll()/afterAll()、beforEach/afterEach()等方法,来完成一个或多个测试所需环境准备,以及关联的清理动作。
Jest 的钩子函数有:
describe(name, fn)
describe.each(table)(name, fn, timeout)
describe.only(name, fn)
describe.only.each(table)(name, fn)
describe.skip(name, fn)
describe.skip.each(table)(name, fn)
beforeAll(fn, timeout)
afterAll(fn, timeout)
beforEach(fn, timeout)
afterEach(fn, timeout)
test(name, fn, timeout)
test.each(table)(name, fn, timeout)
test.skip();
test.skip.each(table)(name, fn)
test.only(table)(name, fn)
test.only.each(table)(name, fn)
test.todo(name)
test.concurrent(name, fn, timeout)
test.concurrent.each(table)(name, fn, timeout)
test.concurrent.only.each(table)(name, fn)
test.concurrent.skip.each(table)(name, fn)
当你向执行指定的一个测试套件或测试用例时可以使用 describe.only或test.only,如果在指定的测试套件或测试用例不想运行,可以使用 describe.skip 或 test.skip 进行跳过不执行。
Jest 匹配器 Expect(断言)
在 jest 单元测试框架中提供了丰富的断言方法:
方法 | 功能 | 示例 | 结果 |
---|---|---|---|
与真假相关匹配器 | |||
toBe(value) | 验证是否相等并且验证是否是绝对相等( === ) | toBe使用示例 | |
toEqual(value) | 验证数据是否相同 | expect(1).toEqual(1) expect(1).toEqual(2) | pass failed |
expect.any(constructor) | 验证数据类型 | expect(“str”).toEqual(expect.any(String) expect(“str”).toEqual(expect.any(String) | pass failed |
toBeNull() | 验证是否为空 | expect(null).toBeNull() expect(1).toBeNull() | pass failed |
toBeUndefined() | 验证是否未定义 | expect(undefined).toBeUndefined() expect(1).toBeUndefined() | pass failed |
toBeTruthy() | 验证是否为真 | expect(true).toBeTruthy() expect(false).toBeTruthy(); | pass failed |
toBeFalsy() | 验证是否为假 | expect(false).toBeTruthy() expect(true).toBeTruthy(); | pass failed |
not | 结果取反 | expect(true).not.toEqual(false) expect(true).toEqual(false) | pass failed |
与数字相关匹配器 | |||
toBeGreaterThan(number) | 匹配内容是否大于预期 | expect(15).toBeGreaterThan(12) | pass |
toBeGreaterThanOrEqual(number) | 匹配内容是否大于等于预期 | expect(15).toBeGreaterThanOrEqual(12) expect(12).toBeGreaterThanOrEqual(12) | pass pass |
toBeLessThan(number) | 匹配内容是否小于预期 | expect(10).toBeLessThan(12) | pass |
toBeLessThanOrEqual(number) | 匹配内容是否大于等于预期 | expect(10).toBeLessThanOrEqual(12) expect(12).toBeLessThanOrEqual(12) | pass failed |
toBeCloseTo(number) | 浮点数匹配器 | expect(0.1+0.2).toBeCloseTo(0.3) expect(0.1+0.2).toEqual(0.3) | pass failed |
与字符串相关的匹配器 | |||
toMatch(regexp | string) | 匹配内容是否包含指定字符串 | expect(‘hello world !!!’).toMatch(‘world’) |
与数字相关的匹配器 | toContain | ||
jest 提供的匹配器语法非常多,没有必要记住所有的匹配器,很多时候我们只需要记住几个核心的匹配器灵活的运用就行,通过的测试逻辑可以使用不同的匹配器来实现,实际应用还是依据你对哪个比较熟悉 。
数据驱动测试 DDT (Data-Driven Tests)
当一组测试用例有固定的测试数据时,就可以通过参数化的方式简化测试用例的编写。 jest 本身时支持参数化的。
数据驱动测试有时又叫参数化测试,常用的数据驱动方式:内部文件、外部文件、数据库
举例使用内部数据进行参数化测试:
test.each([
{a: 1, b: 1, expected: 2},
{a: 1, b: 2, expected: 3},
{a: 2, b: 1, expected: 3},
])('.add($a, $b)', ({a, b, expected}) => {
expect(a + b).toBe(expected);
});
生成测试报告
jest 支持生成多种测试报告。
使用 jest 的插件 jest-stare,只需要在 jest.config.js 文件中添加 jest-stare 的配置信,在运行测试用例运行结束时自动生成测试报告。
配置信息:
module.exports = {
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: true, // 这个可以依据个人需求选择是否输出测试覆盖率
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// jest.config.js 文件中 reporters 可以忽略,可以在 package.json 中添加 jest-stare
"reporters": [
"default",
[
"jest-stare",
{
"resultDir": "results/jest-stare",
"reportTitle": "jest-stare!",
"additionalResultsProcessors": [
"jest-junit"
],
"coverageLink": "../../coverage/lcov-report/index.html",
"jestStareConfigJson": "jest-stare.json",
"jestGlobalConfigJson": "globalStuff.json"
}
]
]
};
jest-stare 测试报告运行效果图:
Jest 运行命令
jest 常用命令
jest -i
jest --watchAll
jest <文件名> (文件名支持模糊匹配)
POM(Page Object Model)页面对象模型
为什么应用 POM ?
当为 Web 页面编写测试时,需要操作该 Web 页面上的元素。然而,如果在测试代码中直接操作 Web 页面上的元素, 那么这样的代码是极其脆弱的,因为 UI 会经常变动。
为什么 POM 是使用 Selenium 的广大同行最公认的一种设计模式?
Page Object 的设计思想上是白元素定位与元素操作进行分层、屏蔽定位器、底层元素的方法和业务逻辑,这样带来的直接好处就是当页面元素发生变化时,只需要维护 page 层的元素定位,而不需要关心在那些测试用例中使用了这些元素。在编写测试用例时,也不需要关心元素如何定位。
POM 优点
- 抽象出页面对象可以最大程度降低开发人员修改页面代码对测试的影响,所以只需要该对应页面定位即可,而不会对测试代码造成影响;
- 可以在多个测试用例中重复使用一部分测试代码;
- 测试代码变得一读、灵活、可维护;
- 不需要直接接触 Selenium。
改进 POM(Page Operation Modul)
当同一页面内部元素过多或存在很多隐藏窗口,再使用之前的 PO 模式将使得元素定位,将使得 page 层的代码很凌乱且不宜与寻找已经定义多的定位。
更新版的 POM 在之前的基础上将 page 层进行模块划分(Modul)。
Operation 的作用就是在将页面中具有重复性操作或常用操作进行封装具体的类,而类的内部实现具体元素的操作方法。
POM 依赖结构
文件存放和名规则
operation 文件夹
文件名:所属板块名 + Operation.ts
Opeartion文件内部:
类名: 功能模块名 + Operation
方法: 操作方式 + 操作图元
属性: 操作方式 + 操作图元
page 文件夹
文件名:所属板块名 + Page.ts
Page 文件内部:
类名:功能模块 + Page
方法: 操作方式 + 操作图元 (类内部方法可不写所属功能名)
属性: 操作方式 + 操作图元 (类内部方法可不写所属功能名)
函数: 功能名 + 操作方法
enum 文件夹
该目录下存放网页 DOM 定位的关键字
testCase 文件夹
testUAT
该目录用于存放流程测试用例,
AUT 测试文件命名:
文件名: uat01_流程名
示例: wire_Attribute.test.st
testSIT
该目录用来存放功能测试用例,每个测试用例能够独立运行。
文件夹命名规则:
文件夹名: 功能名
示例: wire
SIT 测试文件命名:
文件名: 功能名_操作方法
示例: wire_Attribute.test.st
示例
toBe
const value = {
value : 1
}
describe('toBe 使用示例', () => {
// 通过
test('test toBe 1', () => {
expect(value.value).toBe(1);
});
// 不通过
test('test toBe 2', () => {
console.log(value); // out: { value : 1 }
console.log({ value : 1 }); // out: { value : 1 }
expect(value).toBe({ value : 1 }); // failed
});
});
callback
toContain
test('测试 toContain 匹配器', () => {
// toContain 匹配器 值匹配内容是否包含预期值
const arr = ['xiaoming', 'xiaolong', 'xiaowen']
expect(arr).toContain("xiaoming");
});