自动化测试框架[Cypress最佳实践]

设置全局URL

为了绕过同源策略,当Cypress开始运行测试时,会在localhost上打开一个随机端口进行初始化,知道遇见第一个cy.visit()命令里的URL才匹配被测应用程序的URL,可以在cypress.json中设置baseUrl以减少代码冗余

{
	"baseUrl":"https://davieyang.blog.csdn.net"
}

设置好baseUrl后,不仅可以在运行时节省Cypress匹配被测应用程序的URL的时间,还可以在编写待访问的URL时候忽略baseUrl,直接写后面的路径

//设置了baseUrl为https://davieyang.blog.csdn.net
cy.visit('/article/')
//等于与没有设置baseUrl参数的如下代码
cy.visit('https://davieyang.blog.csdn.net/article/')

避免访问多个站点

Cypress不支持在一个测试用例里访问多个不同域名的URL,否则Cypress会直接报错,如果是同一个超域下的不同子域是允许的

it('访问同一个超域下的不同子域是允许的',()=>{
	cy.visit('https://davieyang.blog.csdn.net/')
	cy.visit('https://davieyang.blog.csdn.net/article/details/112464393')
})
it('访问不同的超域会报错', ()=>{
	cy.visit('https://davieyang.blog.csdn.net/')
	cy.visit('https://www.cnblogs.com/davieyang/')
})

特别的显式等待

在其他的测试框架中,如Selenium会经常用到sleep()或者wait(),但是在Cypress中无需使用显式等待,Cypress自带Retry特性

//等待请求网络返回,错误的做法
cy.wait(1000)
//正确的做法是使用别名
cy.server()
cy.route('/accounts/*').as('getAccount')
cy.wait('@getAccount').then(xhr)=>{
	cy.log(xhr)
}

停用条件测试

在实际的测试过程中经常遇到一个场景"判断一个元素是否存在,如果存在则执行A操作,如果不存在则执行B操作",在Cypress中这种场景叫做条件测试但是在Cypress中条件测试被认为是测试执行不稳定的因素,因此在Cypress中建议通过指定前置测试条件来避免不确定行为,也就是说当有A、B两个策略时,指定测试前置条件从而让A或B一定发生

前置条件的构造,可以通过修改DB直接获得,也可以根据业务特性使用API或者UI的方式构造,总之指导思想是A、B必定会执行其一

//如果确定A会发生,则
cy.get('.conditional A Selector').should('exist');
//如果确定A不会发生,则
cy.get('.conditional A Selector').should('not.exist');

实时调试和中断

Cypress有两种Debug方式

.debug()

当定位问题时,可以使用.debug()函数来调试,.debug()命令返回上一条命令产生的结果

//判断login函数是否含href属性
cy.get('#login').debug().should('hava.attr', 'href')

.debugger

Cypress测试代码和被测应用运行在同一个循环中,这意味着可以访问和控制页面上运行着的代码

it('调试'function(){
	cy.visit('/main')
	cy.get('#login')
		.then(($selectedElement)=>{
		//调试
		debugger
		})
})

不能再测试代码中使用debugger,如下写法是不工作的,Cypress是异步的,当执行如下cy.visit()时,Cypress并没有立刻运行测试代码,debugger自然就不会有什么用

it('调试', function(){
	cy.visit('/main')
	debugger
	cy.get('#login')
})

运行时的截图和录屏

Cypress的截图和录屏能力非常强大

默认自动截图

在以cypress run方式运行测试时,测试发生错误的话Cypress会自动截图,并默认保存在/cypress/screenshots/路径下

自定义截图

也可以在代码中自定义截图,无论测试成功或失败,使用.screenshot()即可

.screenshot()
.screenshot(fildName)
.screenshot(options)
.screenshot(fileName,options)

代码示例

//直接截图
cy.screenshot()
//只截某个特定元素
cy.get('#login').screenshot()

fileName是保存图片的名称,图片默认保存在cypress/screenshots文件夹下,可以在cypress.json中修改此路径,配置项是screenshotsFolder,也可以通过options参数来改变screenshot的默认行为

参数默认值描述
LogTRUE在命令日志中显示
Blackout[]此参数接受一个数组类型的字符选择器,该选择器匹配的元素会被涂黑,此选项在capture是runner时不起作用
Capture‘fullPage’决定截图截取测试运行器的哪个部分,此参数仅跟在cy.后使用有效(针对元素属性截图则不起作用,cy.get(‘#login.’).screenshot({capture:‘runner’})),有三个选项,viewport(截图大小是被测应用程序当前视窗大小),fullPage(整个被测程序界面都会被截图),runner(截图包括测试运行器的整个部分),对于在测试失败时自动获取的截图,此选项被强制设置为runner
clipnull用于裁剪最终屏幕截图图像的位置和尺寸(以像素为单位),格式为:{x:0,y:0, width:100, height:100}
disableTimersAndAnimationsTRUE如果为TRUE,则在截屏时禁止JavaScript计时器(setTimeout, setInterval等)和CSS动画运行
paddingnull用于更改元素屏幕截图尺寸的填充,此属性仅适用于元素屏幕截图,格式为:padding:[‘background-color:#ff7f50;’]
scaleFALSE是否缩放应用程序以适合浏览器窗口,当capture为runner时,强制为TRUE
timeoutresponseTimeouttimeout时间
onBeforeScreenshotnull非因测试失败截图钱,要执行的回调函数,档次参数用于元素屏幕截图时,他的参数是被截取的元素,当此参数应用于其他截图时,它的参数是document本身
onAfterScreenshotnull非因测试失败截图后,要执行的回调函数,当此参数应用于元素屏幕截图时,第一个参数是被截取的元素,当此参数用于其他截图时,第一个参数是document本身,第二个参数是有关屏幕截图的属性

通过onBeforeScreenshot和onAfterScreenshot可以在截图发生前或后应用自定义行为

//onBeforeScreenshot
//打印被截取元素的信息
let screenshots=[]
	cy.get('input[name=password]').screenshot({
		onBeforeScreenshot($el){
			screenshots.push($el)
		},
		capture:'runner'
})
cy.log(screenshots)
//onAfterScreenshot
//打印Screenshot中的有关属性信息
let screenshots=[]
	cy.screenshot({
		onAfterScreenshot($el, props){
			screenshots.push(props)
		},
		capture:'runner'
})
cy.log(screenshots)

断言最佳实践

  • Cypress的断言基于当下流行的Chai断言库,并且增加了对Sinon-Chai,Chai-jQuery断言库的和支持
  • Cypress支持BDD(expect/should)和TDD(assert)格式的断言
//形式大致如下
it('俩数相加', function(){
	expect(add(1,2)).to.eq(3)
})
it('俩数相减', function(){
	assert.equal(subtract(5,12), -7, '结果应该是-7')
})

Cypress命令有内置的断言,并且这些断言使得命令自动重试,从而确保命令成功(或者超时后失败),例如

//Cypress会自动等到返回的body里有{name:'davieyang'}
cy.request('/users/1').its('body').should('deep.eq',{name:'davieyang'})

内置的Cypress命令断言

命令断言事件
cy.visit()期望访问返回的status code是200
cy.request()期望远程server存在并且能连通
cy.contains()期望包含某些字符的页面元素能在DOM里找到
cy.gett()期望页面元素最终能在DOM页面里找到
cy.type()期望页面元素最终处于可以输入的状态
cy.click()期望页面元素最终处于可以单击的状态
cy.its()期望能从当前对象最终找到一个属性
Cypress断言方法
隐性断言.should()或者.and()
cy.get('tbody tr:first').should('hava.class', 'active')
//也可以使用.and()把很多的断言链接起来
cy.get('#header a')
	.should('hava.class', 'active')
	.and('hava.attr', 'href', '/users')
显性断言expect

expect允许传入一个特定的对象并且对其进行断言
expect(true).to.be.true

混合使用
cy.get('tbody tr:first').should(($tr)=>{
	expect($tr).to.hava.class('active')
	expect($tr).to.hava.attr('href', '/users')
})

BBD形式的断言

命令实例
notexpect(name).to.not.equal(‘davieyang’)
deepexpect(obj).to.deep.equal({name:‘davieyang’})
nestedexpect({a:{b:[‘x’,‘y’]}}).to.hava.nested.property(‘a.b[1]’)
nestedexpect({a:{b:[‘x’,‘y’]}}).to.nested.include(‘a.b[1]’:‘y’)
orderedexpect([1,2]).to.hava.ordered.members([1,2]).but.not.hava.ordered.members([2,1])
anyexpect(arr).to.hava.any.keys(‘name’, ‘age’)
allexpect(arr).to.hava.all.keys(‘name’, ‘age’)
a(type)Aliases:anexpect(name).to.not.equal(‘davieyang’)
include(hava), Aliases:contain, includes, containsexpect([1,2,3]).to.include(2)
okexpect(undefined).to.not.be.ok
TRUEexpect(true).to.be.true
FALSEexpect(false).to.be.false
nullexpect(null).to.be.null
undefinedexpect(undifined).to.be.undefined
existexpect(myVar).to.exist
emptyexpect([]).to.be.empty

TDD形式的断言

命令实例
.isOk(object, [message])assert.isOk(‘everything’, ‘everything is ok’)
.isNotOk(object, [message])assert.isNotOk(false, ‘this will pass’)
.equal(actual, expected, [message])assert.equal(3,3,‘vales equal’)
.notEqual(actual, expected, [message])assert.notEqual(3,4, ‘values not equal’)
.strictEqual(actual, expected, [message])assert.notStrictEqual(true, true, ‘bolls strict eq’)
.notStrictEqual(actual,expected,[message])assert.notStrictEqual(5,‘5’,‘not strict eq’)
.deepEqual(actual, expected, [message])assert.deepEqual({id:‘1’},{id:‘1’})
.isTrue(value,[message])assert.isTrue(true, ‘this value is true’)

数据驱动策略

将数据保存在前置条件中

利用beforeEach或者before前置函数可以把数据保存在前置条件中

describe('测试数据放在前置条件里',()=>{
	let testData
	beforeEach(()=>{
		testData=[{"name":"davie", "password":"yang"},
		{"name":"alex", "password":"yang"}]
	})
	//循环生成数据
	for(const data in testData){
		it('测试外部数据${data}',()=>{
			cy.login(data.name, data.password)
		})
		}
})

使用fixtures

describre('测试外部数据',()=>{
	it('测试外部数据', function(){
		//example.json存放在cypress/fixtures下
		//example.json是一个类似数组对象
		cy.fixture('example.json').as('testData')
		cy.get('@testData').each((data)=>{
			cy.log(data.name)
			cy.log(data.email)
			}
		)
	})
})

数据保存在自定义文件中

import teatData from '../../settings/user.json'
describe('Test Add User', ()=>{
	for(const data in testData){
		it('测试外部数据${data}',()=>{
			cy.login(data.user.admin.email, data.user.admin.password)
		})
	}
})

环境变量设置

cypress.env.json

Cypress允许你针对不同测试环境使用多个配置文件并且在运行时动态指定,首先在新建\cypress\config\路径,并在config下新建两个json文件,并命名为cypress.dev.jsoncypress.test.json

//cypress.dev.json
{
	"baseUrl":"http://localhost:7077/login"
	"env":{
		"username":"davieyang",
		"password":"alexyang"
	}
}
//cypress.test.json
{
	"baseUrl":"http://localhost:7077/login"
	"env":{
		"username":"alexyang",
		"password":"davieyang"
	}
}

然后配置/plugins/index.js文件

const fs = require('fs-extra')
const path = require('path')
function getConfigurationByFile(file){
	const pathToConfigFile = path.resolve('..','Cypress/cypress/config', `cypress.${file}.json`)
	return fs.readJson(pathToConfigFile)
}
//plugins file
module.exports = (on, config)=>{
	//指定默认环境配置,使用cypress.dev.json
	const file = config.env.configFile || 'dev'
	return getConfigurationByFile(file)
}

实际运行测试时,使用命令yarn cypress run --env configFile=test指定使用cypress.test.json环境运行测试

运行时动态指定环境变量

使用cypress.evn.json可以指定测试环境运行,但需要额外创建文件,除了使用cypress.evn.json外,在运行时指定测试环境的同时仍然可以使用cypress.json文件

//建立一个变量targetEnv,并给定默认值dev环境
//更改env代码块,将环境及环境变量按格式写入
"targetEnv":"dev"
"env":{
	"dev":{
		"username":"davieyang",
		"password":"831119@#",
		"Url":"http://localhost:7077",
		},
	"test":{
		"username":"davieyang_test",
		"password":"831119@#_test",
		"Url":"http://localhost:7078",
		}
}

然后更改/support/index.js文件

//使用用户的参数testEnv,如果没有指定testEnv,则使用cypress.json中的targetEnv的设置
//根据最终的环境变量重写url
beforeEach(()=>{
	const targetEnv=Cypress.env('testEnv')||Cypress.config('testEnv')
	cy.log('测试环境为:\n ${JSON.stringify(targetEnv)}')
	cy.log('测试环境详细配置为:\n ${JSON.stringify(Cypress.env(targetEnv))}')
	Cypress.config('baseUrl', Cypress.env(targetEnv).Url)
})

实际执行的时候运行如下命令
yarn cypress:open --env testEnv=test

测试运行最佳实践

静态挑选待运行的测试用例

指的是给测试用例添加关键字,例如describe.only(), describe.skip(), it.only(), it.skip()及给测试用例指定runFlag, 并在运行时指定runFlag的值

详情可查看自动化测试框架[Cypress测试用例]

动态挑选待运行的测试用例

安装插件

npm install --save-dev cypress-select-tests

配置插件

修改cypress/plugins/index.js文件

const selectTestsWithGrep = require('cypress-select-tests/grep')
module.exports = (on, config)=>{
	on('file:preprocessor', selectTestsWithGrep(config))
}
测试用例

在integration目录下新建testPickToRun.js

///<reference types="cypress"/>
describe('测试登陆', function(){
	const username = 'davie.yang',
	const password = '831119@#',
	context('登陆成功, 跳转到Dashboard页', function(){
		it("['smoke']TestLogin1", function(){
			cy.visit('http://localhost:7077/login')
			cy.get('input[name=username]').type(username)
			cy.get('input[name=password]').type(password)
			cy.get('form').submit()
			//验证登陆成功则跳转到dashboard页
			cy.get('h1').should('contain','davie.yang')
		})
		it("[e2e, 'smoke']TestLogin2", function(){
			cy.log('davieyang')
		})
	})
})
用例执行

实际执行使用命令yarn cypress:open --env grep=e2e

根据文件名来执行用例

执行所有文件名中包含Login字符的文件
yarn cypress:open --env fgrep=Login

测试运行失败自动重试

安装插件

npm install -D cypress-plugin-retries

配置插件

cypress/support/index.js新增配置
require('cypress-plugin-retries')
package.jsonscripts代码块中新增配置

{
	"scripts":{
		"retryCases":"CYPRESS_RETIRES=2 cypress run"
	}
}

执行用例

yarn retryCases

如此所有测试用例执行失败后都会重试2次

Cypress链接DB

Cypress使用cy.task()来链接DB,语法如下

cy.task(event)
cy.task(event, arg)
cy.task(event, arg, options)

代码实例

执行task

/plugins/index.js中,修改如下代码

//定义一个task
module.exports=(on, config)=>{
	on('task',{
		log(message){
			console.log(message)
			return null
		}
	})
}

然后测试代码便可以如此使用

//仅列出测试语句
cy.task('log','Test Log Task')
连接MySQL
安装插件

npm install --save-dev mysql

配置插件

cypress.json的evn变量中,配置如下内容

"db":{
	"host":"host_address",
	"user":"username",
	"password":"password",
	"database":"db"
	
}

修改/plugins/index.js文件

const mysql=require('mysql')
function queryTestDB(query, config){
	//创建一个新的MySQL连接,配置取自cypress.json的env中db部分
	const dbconnection = mysql.createConnection(config.env.db)
	//建立连接
	dbconnection.connect()
	//执行DB查询,并在结束后断开
	return new Promise((resolve, reject)=>{
		dbconnection.query(query, (error, results)=>{
		 if(error)reject(error)
		 else{
			dbconnection.end()
			console.log(results)
			return resolve(results)
			}
		})
	})
}
module.export = (on, config)=>{
	on('task',{
		queryDb:query=>{
			return queryTestDb(query, config)
		},
	})
}
测试用例
var query = 'select * from table'
cy.task('queryDb', query)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Davieyang.D.Y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值