自动化测试

自动化测试是指搭建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函数)

利用其钩子函数可以在所有的测试用例前/后做一些操作;

  • beforeafter
    describe()context()内只执行一次,在所有的it之前执行;
  • beforeEachafterEach
    一个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的地址

筛选用例skiponly

skip跳过该用例,only只执行该用例

it.skip('进入的事件 - skip', () => {
	cy.log('进入百度')
});
it.only('进入的事件', () => {
	cy.log('进入百度')
});

用例的展示如下:

cypress 生成报告 cypress开发_UI

获取元素,元素定位

常规的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);