自动化测试
自动化测试是指搭建cypress
的环境,去书写流程,并比较实际与预期结果之间的差异。通过cypress
,可以把人对软件的测试行为转化为由机器自动执行测试的行为,从而替代大量的手工测试操作,使得测试可以快速,反复的进行;并可以具体的查看其中的某一个步骤的测试结果;
Cypress简介
-
Cypress
是为现代网络打造的,基于JavaScript
的下一代前端测试工具。他可以对浏览器中运行的任何内容进行快速,简单和可靠的测试。 -
Cypress
是自集成的,它提供了一套完整的端到端测试体验。无须借助其他外部工具,在简单安装后即可允许用户快速的创建、编写、运行、测试用例,并且针对每一步操作均支持回看。 - 不同于其他只能测试UI层的前端测试工具,
Cypress
允许你编写所有类型的测试,覆盖了测试金字塔模型涉及的所有测试类型:端到端测试、集成测试、单元测试;
如果想从头学起Cypress,可以点击此处翻看系列文章哦
点击进入官方网站,运行npm install cypress
安装;cypress
现目前没有中文的文档,在国内的资料也较少;但是cypress
语法酷似JQuery
的形式,翻看API可以很快的上手项目的书写;点击进入查看Cypress API,官方所给的示例中有事件的案例,可结合项目进行开发与书写示例;
项目目录:
- fixtyres – 测试家具,需要依赖的静态数据(相当于mock的数据)
- plugins – 书写插件,扩展cypress功能
- support – 支持文件,配置通用信息(每次运行的时候都会加载)
- interaction – 测试脚本(用例)
- cypress.json – 自定义
cypress
运行项目:npm run open
;在package.json中配置了"open": "cypress open"
在integration
文件夹下新建.js的文件,写上一个进入百度页面的测试用例,如下:
describe('百度的测试用例', () => {
beforeEach(() => {
cy.visit('https://www.baidu.com/')
})
it('进入的事件', () => {
cy.log('进入百度')
});
});
生命周期(HOOK函数)
利用其钩子函数可以在所有的测试用例前/后做一些操作;
- before与 after
在describe()
或context()
内只执行一次,在所有的it之前执行; - beforeEach与afterEach
一个describe()
或context()
内有多少个测试用例it
,就会执行多少次;
配置cypress.json
的文件
{
"env": {
"ENV": "test",
"ajaxUrl": "https://****/",
"postUrl": "https://****/Gateway.fcgi?method=",
"curUrl" : "https://*****"
},
"video": false,
"baseUrl": "https://****.com", // 默认
"userAgent": "Mozilla/5.0 (Linux; Android 11; NTH-AN00 Build/HONORNTH-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/3195 MMWEBSDK/20220204 Mobile Safari/537.36 MMWEBID/9183 MicroMessenger/8.0.20.2100(0x28001451) Process/tools WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64",
"viewportHeight": 740,
"viewportWidth": 360,
}
使用配置的信息:
cy.visit(); // 括号为空时会跳转到cypress.json所配置的baseUrl的地址
Cypress.env('curUrl'); // 获取env中curUrl的地址
筛选用例skip
与only
skip
跳过该用例,only
只执行该用例
it.skip('进入的事件 - skip', () => {
cy.log('进入百度')
});
it.only('进入的事件', () => {
cy.log('进入百度')
});
用例的展示如下:
获取元素,元素定位
常规的css选择器的模式,cy.get(select)
,还提供了JQuery的选择器;点击查看.get()的API
cy.get('#query-btn').should('contain', 'Button')
cy.get('.query-btn').should('contain', 'Button')
cy.get('#querying .well>button:first').should('contain', 'Button')
// 也可满足,与JQuery的选择元素相类似
cy.get(select); // 会匹配多个元素
cy.get(select).find(select);
cy.get(select).first();
.....
cy.get(select).eq();
// 遍历获取的元素
cy.get(select).each(($item) => {
cy.log($item.text())
});
点击元素与滑动
元素的点击事件, 有方位与双击的点击实例;点击查看.click()的API
cy.get('.item').click('top');
cy.get('#action-canvas').click('topLeft')
cy.get('#action-canvas').click('top')
cy.get('#action-canvas').click('topRight')
cy.get('#action-canvas').click('left')
cy.get('#action-canvas').click('right')
cy.get('#action-canvas').click('bottomLeft')
cy.get('#action-canvas').click('bottom')
cy.get('#action-canvas').click('bottomRight')
// if you chain .scrollTo() off of cy, we will
// scroll the entire window
cy.scrollTo('bottom')
cy.get('#scrollable-horizontal').scrollTo('right')
// or you can scroll to a specific coordinate:
// (x axis, y axis) in pixels
cy.get('#scrollable-vertical').scrollTo(250, 250)
// or you can scroll to a specific percentage
// of the (width, height) of the element
cy.get('#scrollable-both').scrollTo('75%', '25%')
// control the easing of the scroll (default is 'swing')
cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' })
// control the duration of the scroll (in ms)
cy.get('#scrollable-both').scrollTo('center', { duration: 2000 })
浏览器的操作
cy.window(); // 获取当前页面的窗口对象
cy.title(); // 获取title
cy.url(); // 获取当前页面的url
cy.go(); // 浏览器的前进后退
cy.reload(); // 刷新页面
cy.viewport(820, 1180); // 设置窗口大小
cy.wait(); // 强制等待
API使用实例
断言的使用:
// 1.常规的断言should()
cy.getCookie('token').should('have.property', 'value', '123ABC');
cy.getCookies().should('be.empty');
// cy.getCookies() yields an array of cookies
// 2.BDD断言 expect()
cy.getCookies().should('have.length', 1).should((cookies) => {
// each cookie has these properties
expect(cookies[0]).to.have.property('name', 'token')
expect(cookies[0]).to.have.property('value', '123ABC')
expect(cookies[0]).to.have.property('httpOnly', false)
expect(cookies[0]).to.have.property('secure', false)
expect(cookies[0]).to.have.property('domain')
expect(cookies[0]).to.have.property('path')
})
// 3.TDD断言
assert.equal(2, 2, '俩个值相等');
assert.notEqual(3, 2, '俩个值不相等');
assert.deepEqual({'id': '1'}, {'id': '1'});
assert.isTure(1 == 1, '判断表达式为true');
assert.isFalse(1 == 2, '判断表达式为false');
as()别名的使用;
cy.get('#start-btn').as('btn'); // .as()的别名
cy.get('@btn').click(); // 别名的使用
invoke()的使用;也可进行执行函数
cy.get('.connectors-div').should('be.hidden')
// call the jquery method 'show' on the 'div.container'
.invoke('show')
.should('be.visible');
cy.get('.connectors-div')
.should('be.hidden')
.invoke('show')
.should('be.visible')
cy.setCookie('_id', '1');
cy.intercept(/method=queryUser/, responseData({})).as('result'); // 接口回报重定向回包内容
cy.get('@submitBtn').click(); // 提交的click()
cy.wait('@result'); // 等待接口的回包响应
// 继续执行事例断言,验证回包的内容
截图的命名:cy.screenshot('截取当前页面图');
运行会截取当前的页面图;
重试与超时的执行的命令:
it('重试操作', {
retries: 3, // 重试次数
defaultCommandTimeout: 1000
}, function() {
assert.notEqual(3, 2, '俩个值不相等');
});
项目开发经验
页面的功能的测试,主要划分为三个方面:
- UI界面的测试(UI.spec.js) - 元素是否包含特定属性,屏幕尺寸更改的适配,元素是否存在展示断言;
- 功能交互测试(func.spec.js) - 正常逻辑的交互的接口请求(成功/失败);
- 环境进入的测试(jsBridge.spec.js) - 个别进入到页面所需要特定的环境配置;例如:qq/wx
进入页面的visit()
方法,访问页面进入的接口访问及更改配置的通用:
function initEnv(url, { isPCLoad = false }) {
cy.setCookie('session_id', '1'); // 添加本地cookie的存储
cy.setCookie('channel_id', '1');
cy.intercept(/method=query***Info/, {
ret: 0,
msg: '认证成功'
}).as('result'); // 请求接口的回包内容
cy.visit(url, {
onBeforeLoad (win) {
// 默认全局的配置是移动端,在这添加方法的参数进行更改为pc的进入;
isPCLoad && Object.defineProperty(win.navigator, 'userAgent', {
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
});
}
});
}
// 进入时调用此方法
beforeEach(() => {
initEnv(url, {});
});
userAgent
的配置
// 移动端
"userAgent": "Mozilla/5.0 (Linux; Android 11; NTH-AN00 Build/HONORNTH-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/3195 MMWEBSDK/20220204 Mobile Safari/537.36 MMWEBID/9183 MicroMessenger/8.0.20.2100(0x28001451) Process/tools WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64",
// pc端
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36",
测试用例
公用文件内容:
// initPage > recognition .js
const { userRes_0 } = require('../apiResponse/recognition'); // 回包做为单独的文件进行引入
// 统一查找dom的方法,并使用`.as()`进行别名的书写,增加语句书写的可读性
function initDom() {
cy.get('#loading').as('loading'); // loading的加载img
cy.get('#app').as('app'); // 认证内容
// .....
}
function initEnv(url, { isPCLoad = false }) {
cy.setCookie('session_id', '1');
cy.setCookie('channel_id', '1');
cy.intercept(/method=query****Info/, userRes_0 ).as('result');
cy.visit(url, {
onBeforeLoad (win) {
isPCLoad && Object.defineProperty(win.navigator, 'userAgent', {
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
});
}
});
}
module.exports = {
initDom,
initEnv,
};
回报内容做为单独的文件引用,清晰目录的更改;
// apiResponse > recognition.js
const getUserRes = ({
ret = 0,
msg = 'success'
}) => {
return {
ret,
msg,
traceId: "Gateway-test",
};
};
const user***Res_0 = getUserRes ({});
const user***Res_83000= getUserRes ({
ret: 83000,
msg: '当前页面加载异常,....'
});
// 单独回报对象的导出
export default {
userRes_0,
userRes_83000
}
UI.spec.js
- UI测试用例
// recognition > ui.spec.js
const { initDom, initEnv } = require('../../../initPage/recognition .js');
describe('check ui', () => {
context('userAgent - 移动端的配置', function () {
beforeEach(() => {
initEnv(url, {});
});
// 检测页面上的UI是否正常展示
function checkUI() {
initDom();
cy.get('@loading').should('not.be.visible');
cy.get('@app').should('be.visible');
// .....
}
it('页面展示元素', () => {
checkUI();
});
it('iphone 12 pro', () => {
cy.viewport(390, 844);
checkUI();
});
it('iphone 12 pro --half', () => {
cy.viewport(390, 400);
cy.get('.ct__main')
.first()
.scrollTo('bottom');
checkUI();
});
// ...多个尺寸的适配
})
});
func.spec.js
- 事件测试用例
// recognition > func.spec.js
const { initDom, initEnv } = require('../../../initPage/recognition.js');
const {
doUserRes_0,
userRes_83000,
userRes_2006
} = require('../../../apiResponse/recognition');
// 在页面的输入框中输入内容
function inputForm(){
initDom();
initEnv(`${url}?hasLogin=true`, {}); // 进入页面
cy.get('@NameInput').type('张三');
cy.get('@cardIDInput').type('4452563****45666');
// ...填写其它需要提交的内容
}
// 提交事件 - 接口访问与回包
function submitBtnClick(response = doUserRes_0){
return new Cypress.Promise((resolve) => {
cy.intercept(/method=doUser***/, response).as('result');
cy.get('@submitBtn').click();
cy.wait('@result');
resolve();
});
};
describe('example to-do app_cert', () => {
it('页面功能测试', () => {
inputForm();
cy.then(() => {
const msg = '当前页面加载异常,....';
submitBtnClick(userRes_83000).then(() => {});
});
// 登录信息已失效
cy.then(() => {
const msg = '您的登录信息已失效,请重新登录';
submitBtnClick(userRes_2006).then(() => {});
});
});
});
jsBridge.spec.js
- 环境配置测试用例
const { initDom } = require('../../../initPage/app_cert.js');
const { userRes_1 } = require("../../../apiResponse/app_cert");
const Util = require('../../../utils/util');
const config = require('../../../../script/config');
const url = `${config.test.app.url}?hopeToken=22&algorithm=v2`;
const msdkVersion = Util.msdkVersion(url);
let msdkApi = '';
describe('app_cert实名验证页面 - jsBridge', () => {
const onBeforeLoad = function(win) {
cy.stub(win, 'prompt').as('prompt').returns('my custom message');
cy.stub(win.console, 'log').as('log');
}
beforeEach(() => {
// 引入json的文件路径,根据其类型拿到对应的data,进行环境的断言
cy.fixture('msdkApi.json').then(res => {
msdkApi = res[msdkVersion];
});
cy.intercept(/method=queryUser***/, userRes_1 ).as('result');
cy.visit(url, { onBeforeLoad });
});
it('页面jsBridge测试', () => {
initDom();
cy.resultIt(true); // 改方法为自行写入到cy的通用方法,如后介绍
cy.get('@prompt')
.should('be.calledWith', msdkApi.certLoginFail)
.should('be.calledWith', msdkApi.closeWebview);
cy.get('@log')
.should('be.calledWith', 'jkxtMethod:onClosePage');
});
});
写入到cypress的通用Commands
方法,全局都可使用;
// support > commands.js
/**
* 认证的结果
* @param {*} resultIt
*/
Cypress.Commands.add('resultIt', (resultBool) => {
const resultTitle = resultBool ? '认证成功' : '认证失败';
cy.get('@faceResult')
.then(($result) => {
cy.wrap($result.get(0))
.find('.res__tt')
.should('have.text', resultTitle);
cy.wrap($result.get(0))
.find('.btn__act')
.should('have.text', '确认')
.click();
});
});
// 使用方式
cy.resultIt(true);