设置全局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的默认行为

参数

默认值

描述

Log

TRUE

在命令日志中显示

Blackout

[]

此参数接受一个数组类型的字符选择器,该选择器匹配的元素会被涂黑,此选项在capture是runner时不起作用

Capture

‘fullPage’

决定截图截取测试运行器的哪个部分,此参数仅跟在​​cy.​​后使用有效(针对元素属性截图则不起作用,cy.get(’#login.’).screenshot({capture:‘runner’})),有三个选项,viewport(截图大小是被测应用程序当前视窗大小),fullPage(整个被测程序界面都会被截图),runner(截图包括测试运行器的整个部分),对于在测试失败时自动获取的截图,此选项被强制设置为runner

clip

null

用于裁剪最终屏幕截图图像的位置和尺寸(以像素为单位),格式为:{x:0,y:0, width:100, height:100}

disableTimersAndAnimations

TRUE

如果为TRUE,则在截屏时禁止JavaScript计时器(setTimeout, setInterval等)和CSS动画运行

padding

null

用于更改元素屏幕截图尺寸的填充,此属性仅适用于元素屏幕截图,格式为:padding:[‘background-color:#ff7f50;’]

scale

FALSE

是否缩放应用程序以适合浏览器窗口,当capture为runner时,强制为TRUE

timeout

responseTimeout

timeout时间

onBeforeScreenshot

null

非因测试失败截图钱,要执行的回调函数,档次参数用于元素屏幕截图时,他的参数是被截取的元素,当此参数应用于其他截图时,它的参数是document本身

onAfterScreenshot

null

非因测试失败截图后,要执行的回调函数,当此参数应用于元素屏幕截图时,第一个参数是被截取的元素,当此参数用于其他截图时,第一个参数是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形式的断言

命令

实例

not

expect(name).to.not.equal(‘davieyang’)

deep

expect(obj).to.deep.equal({name:‘davieyang’})

nested

expect({a:{b:[‘x’,‘y’]}}).to.hava.nested.property(‘a.b[1]’)

nested

expect({a:{b:[‘x’,‘y’]}}).to.nested.include(‘a.b[1]’:‘y’)

ordered

expect([1,2]).to.hava.ordered.members([1,2]).but.not.hava.ordered.members([2,1])

any

expect(arr).to.hava.any.keys(‘name’, ‘age’)

all

expect(arr).to.hava.all.keys(‘name’, ‘age’)

a(type)Aliases:an

expect(name).to.not.equal(‘davieyang’)

include(hava), Aliases:contain, includes, contains

expect([1,2,3]).to.include(2)

ok

expect(undefined).to.not.be.ok

TRUE

expect(true).to.be.true

FALSE

expect(false).to.be.false

null

expect(null).to.be.null

undefined

expect(undifined).to.be.undefined

exist

expect(myVar).to.exist

empty

expect([]).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.json​​​和​​cypress.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.json​​的​​scripts​​代码块中新增配置

{
"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)