前端自动化测试概述
- MVC(Model View Controller)模式开始流行。MVC是模型(Model)、视图(View)和控制器(Controller)的缩写,它使业务逻辑、数据、界面显示分离。这时Web开发属于View层。
- Ajax(Asynchronous JavaScript and XML)的出现改变了上述情况,作为一种能够创建交互式网页应用的网页开发技术,它使用户操作与服务器响应异步化。特别是随着Gmail这个里程碑式产品的使用,Web开发全面进入了“响应式页面”时代,也催生了前端开发这个岗位
- 而SPA(Single Page Application)的出现,则让前端有了应用程序的雏形。SPA能够加载单个HTML页面并在用户与应用程序交互时动态更新该页面。在SPA之后,前端开发变成了前端应用开发。
- Angular、Vue、Node.js的出现彻底改变了前端技术。特别是Node.js(Node.js是一个支持JavaScript运行在服务器端的开发平台)的出现,使得前端开发也可以编写后端程序,JavaScript事实上也成为服务器端开发语言。
- 前端自动化测试是针对前端代码的测试(目前最流行的前端语言是JavaScript)。因为JavaScript事实上已经不再只限于前端的开发,也可以胜任后端的开发,再加上Node.js的出现让更多的项目中出现了由前端开发者负责的BFF(Backend For Frontend,服务于前端的后端)层,因此前端的自动化测试就自然而然地扩展了。前端自动化测试不仅包括UI自动化测试,还可以包括API,集成测试和单元测试。也就是说,前端自动化的可覆盖范围,应包括测试金字塔的每一层。
- Selenium/WebDriver本身却仍只能单纯地用在UI测试层面(除非加入第三方库)。
- 前端测试框架并没有与时俱进,于是我们常常遇见这样的问题:一个接口测试请求失败了,我们不知道是前端的问题还是后端的问题,测试人员需要花费大量时间排查
- 随着上述问题越来越多,Selenium/WebDriver越来越不能满足整个测试行业对于前端自动化框架的需求。于是有追求的优秀企业及个人,依托于现代Web技术的发展,开始寻找或者创建更能适应当前前端开发趋势的前端测试框架。
- 例如Karma,Nightwatch,Protractor,TestCafe,Cypress和Puppeteer。在这些测试工具中,有的仍然依托于Selenium/WebDriver的底层协议,有的则完全自成体系,它们或极大地扩展了原有Selenium/WebDriver的功能,或填补了Selenium/ WebDriver由于架构设计而无法弥补的空白。
- 例如Karma,Nightwatch,Protractor,TestCafe,Cypress和Puppeteer。在这些测试工具中,有的仍然依托于Selenium/WebDriver的底层协议,有的则完全自成体系,它们或极大地扩展了原有Selenium/WebDriver的功能,或填补了Selenium/ WebDriver由于架构设计而无法弥补的空白。
javascript基础(1.3,异步与闭包概念)
异步(Async) JavaScript是单线程执行式语言,这就意味着任何一个函数都要从头到尾执行完毕之后,才会执行另一个函数。假设有一段代码需要接收用户的输入执行,那么在用户输入这段时间,JavaScript就会阻塞自己接受新的任务,这完全不能接受。于是JavaScript把任务分成了两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程而进入“任务队列”的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行,这就是JavaScript的异步机制。
JavaScript异步机制的原理如下: ? 作为单线程语言,在JavaScript里定义的所有同步任务都在主线程上执行,形成一个执行栈。
? 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
? 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务于是结束等待状态,进入执行栈,开始执行。
? 主线程不断重复上面的第三步。
闭包(closure)
iTesting function outer( ){
var name = 'iTesting';
function inner( ){
console.log(name)
}
return inner
}
var closureExample = outer( ) closureExample( )
笔者定义了一个外部函数outer和一个内部函数inner。在外部函数outer内部,定义了一个局部变量name,并且在内部函数inner里引用了这个变量,最后我设置外部函数outer的返回值是内部函数inner本身,这就是闭包。
简化一下,可以理解为闭包就是满足以下条件的函数:
? 在一个函数的内部定义一个内部函数,并且内部函数里包含对外部函数的访问。
? 外部函数的返回值是内部函数本身。
闭包有什么作用呢?闭包允许你在一个函数的外部访问它的内部变量。
cypress简介
- 大多数测试工具(如Selenium/WebDriver)通过在浏览器外部运行并在网络上执行远程命令来运行[5]。Cypress恰恰相反,Cypress在与应用程序相同的生命周期里执行。
- 从技术上讲,当你运行测试时,Cypress首先使用webpack将测试代码中的所有模块bundle到一个js文件中,然后,它会运行浏览器,并且将测试代码注入一个空白页面里,然后它将在浏览器中运行测试代码(可以理解为Cypress通过一系列操作将测试代码放到一个iframe(内嵌框架)中运行)。
- 在每次测试首次加载Cypress时,内部Cypress Web应用程序先把自己托管在本地的一个随机端口上(类似于http://localhost:65874/__/),在识别出测试中发出的第一个cy.visit( )命令后,Cypress将会更改其本地URL以匹配你远程应用程序的Origin(用于满足同源策略),这使得你的测试代码和应用程序可以在同一个Run Loop中运行
- 因为Cypress测试代码和应用程序均运行在由Cypress全权控制的浏览器中,且它们运行在同一个Domain(域)下的不同iframe内
cypress局限
? 不建议使用Cypress用于网站爬虫,性能测试之目的。
? Cypress永远不会支持多标签测试。
? Cypress不支持同时打开两个及以上的浏览器。
? 每个Cypress测试用例应遵守同源策略(same-origin policy)[8]。
? 目前浏览器支持Chrome,Firefox,Microsoft Edge和Electron。
? 不支持测试移动端应用。
? 针对iframe的支持有限。
? 不能在window.fetch上使用cy.route( )。
? 没有影子DOM支持。
同源策略指协议相同,域名相同,端口相同。
快速定位页面元素
下图为一个例子
describe('登陆',()=>{
const username = 'java,lane'
const passward = 'password123'
context('HTML'表单登陆测试, ()=>{
it('login sucess, turn to dashboard page', => (){
cy.visit("http://localhost:7077/login")
cy.get('input[name=username]').type(username)
cy.get('input[name = password]').type(password)
cy.get('form').submit()
cy.url().should('include','/dashboard')
cy.get('h1').should('contains','java.lane')
})
})
})
cypress调试
下面简要介绍一下Cypress提供的这些调试能力。
? 每个命令(Command)均有快照且支持回放
以图3-8为例,Cypress记录了每一个操作命令执行时的快照,并支持在不同操作命令快照之间切换,方便开发者了解整个测试的上下文信息。
? 支持查看测试运行时发生的特殊页面事件(例如网络请求)
Cypress会记录测试运行时发生的特殊页面事件,包括:
? 网络XHR请求。
? URL哈希更改。
? 页面加载。
? 表格提交。
例如在本例中,单击“SUMBIT”按钮后产生的就是表格提交请求,如图3-9所示。 Console输出每个命令(Command)的详细信息
仍以图3-9为例,Cypress除了记录“submitting form”这个表格提交请求,还在Console里打印出了这个请求的详细信息,可以进一步帮助开发者了解系统在运行时的详细状态信息。
? 暂停命令(Command)并单步/恢复执行
在调试测试代码时,Cypress提供了如下两个命令来暂停。
? cy.pause( )
把cy.pause( )添加到testLogin.js文件中,位置置于cy.get(‘form’).submit( )之前。
留意图3-10中左上角Paused标记,它的右边分别是“Resume”和“Next:‘get’”按钮。如果选择“Resume”按钮并单击,测试将恢复运行直至运行结束。如果选择“Next:‘get’”按钮并单击,测试会变成单步执行,即单击后,会执行cy.get(‘form’)请求,再次单击会执行submit动作。
想在哪儿暂停在语句下面加一行cy.pause()
更改username定位器,使其不止匹配一个元素
describe('登陆',()=>{
const username = 'java,lane'
const passward = 'password123'
context('HTML'表单登陆测试, ()=>{
it('login sucess, turn to dashboard page', => (){
cy.visit("http://localhost:7077/login")
cy.get('input').type(username)
cy.get('input[name = password]').type(password)
cy.get('form').submit()
cy.url().should('include','/dashboard')
cy.get('h1').should('contains','java.lane')
})
}
}
结果如下
因为不止一个元素满足要求,故执行下一命令type时测试以失败结束。
测试框架
上图为我的vscode,下图为装好cypress自动生成的文件结构。
fixtures(测试夹具)
测试夹具通常配合cy.fixture( )命令使用,主要用来存储测试用例的外部静态数据。
测试夹具默认位于cypress/fixtures中,但可以配置到另一个目录。
测试夹具里的静态数据通常存储在.json后缀文件里(例如自动生成的examples.json文件)。这部分数据通常是某个网络请求的对应响应部分,包括HTTP状态码和返回值,一般是复制过来更改而不由用户手工填写。
如果你的测试需要对某些外部接口进行访问并依赖于它的返回值,则可以使用测试夹具而无须真正地访问这个接口。
使用测试夹具有如下几个好处:
? 消除了对外部功能模块的依赖。
? 你编写的测试用例可以使用测试夹具提供的固定返回值,并且你确切知道这个返回值是你想要的。
? 因为无须真正地发送网络请求从而使测试更快。
integration(测试文件)
测试文件其实就是我们的测试用例。它默认位于cypress/integration中,但可以配置到另一个目录。所有位于cypress/integration文件夹下,以如下后缀结尾的文件都将被Cypress视为测试文件:
? .js文件。是以普通JavaScript编写的文件。
? .jsx文件。是带有扩展的JavaScript文件,其中可包含处理XML的ECMAScript。
? .coffee文件。是一套JavaScript的转译语言,相对于JavaScript,它拥有更严格的语法。
? .cjsx文件。CoffeeScript中的jsx文件。
要创建一个测试文件很简单,只要创建一个以上述后缀结尾的文件即可。
插件文件(Plugin file)
Cypress独一无二的优点是,测试代码运行在浏览器之内,这使得Cypress跟其他的测试框架相比,有着显著的架构优势。
尽管这提供了更加可靠的测试体验,并使编写测试变得更加容易,但这确实使在浏览器之外进行通信更加困难。
Cypress注意到了这个痛点,所以提供了一些现成的插件(Plugins),使你可以修改或者扩展Cypress的内部行为(例如动态修改配置信息和环境变量等),也可以自定义自己的插件。
默认状态,插件位于cypress/plugins/index.js中,但可以配置到另一个目录。为了方便起见,在每个测试文件运行之前,Cypress都会自动加载插件文件cypress/plugins/index.js。
插件在Cypress中的典型应用有:
? 动态更改来自cypress.json,cypress.env.json,CLI或系统环境变量的已解析配置和环境变量。
? 修改特定浏览器的启动参数。
? 将消息直接从测试代码传递到后端。
支持文件(Support file)
支持文件目录是放置可重用配置例如底层通用函数或全局默认配置的绝佳地方。
支持文件默认位于cypress/support/index.js中,但可以配置到另一个目录。为了方便起见,在每个测试文件运行之前,Cypress都会自动加载支持文件cypress/ support/index.js。
使用支持文件非常简单,只需要在cypress/support/index.js文件里添加beforeEach( )函数即可。例如增加下列代码到cypress/support/index.js中,将能实现每次测试运行前打印出所有的环境变量信息。
beforeEache(=>(){
cy.log('当前的环境变量为${JSON.stringify(Cypress.env( ))))‘
})
cypress.json(自定义配置文件)
cypress.config()
重试机制(cypress核心之一)
describe('登陆',()=>{
const username = 'java,lane'
const passward = 'password123'
context('HTML'表单登陆测试, ()=>{
it('login sucess, turn to dashboard page', => (){
cy.visit("http://localhost:7077/login")
cy.get('input[name=username]').type(username)
cy.get('input[name = password]').type(password)
cy.get('form').submit()
cy.url().should('include','/dashboard')
cy.get('h1').should('contains','java.lane')
})
})
})
最后一个断言,检查标签为“h1”的元素中是否包含“jane.lane”。 断言的一般步骤为用命令cy.get( )查询应用程序的DOM,找到与选择器匹配的元素,然后针对匹配到的元素或元素列表进行断言尝试(在我们的示例中为.should(‘contain’, ‘jane.lane’))。 由于现代web应用程序几乎都是异步的,请试想一下如下情况:
如果断言发生时应用程序尚未更新DOM怎么办?
如果断言发生时应用程序正在等待其后端响应,而导致页面暂无结果怎么办?
如果断言发生时应用程序正在进行密集计算,而导致页面未及时更新怎么办?
这些情况在现实测试中经常会发生,一般的处理方式是在断言前加个固定等待时间(通常硬编码,但仍有可能会发生测试失败),但Cypress更加智能。在实际运行中,如果cy.get( )命令之后的断言通过,则该命令成功完成。如果cy.get( )命令后面的断言失败,则cy.get( )命令将重新查询应用程序的DOM。然后,Cypress将尝试对cy.get( )返回的元素进行断言。如果断言仍然失败,则cy.get( )将尝试重新查询DOM,依此类推,直到断言成功或者cy.get( )命令超时为止。
与其他的测试框架相比,Cypress的这种“自动”重试能力避免了在测试代码中编写硬编码(hard code)等待,使测试代码更加健壮。
多重断言
在日常的测试中,有时候需要多重断言,即单个命令后跟多个断言。在断言时,Cypress将按顺序重试每个命令。即当第一个断言通过后,在进行第二个断言时仍会重试第一个断言。当第一和第二断言通过后,在进行第三个断言时会重试第一和第二个断言,依此类推。
假设一个下拉列表,存在两个选项,第一个选项是“iTesting”,第二个选项是“testerTalk”。我们需要验证这两个选项存在,并且顺序正确,则代码片段如下:
cy.get('.list>li')
.should('have.length'.2)
.and(($li) =>{
expect($li.get()).textContent.'first item').to.equal('iTesting')
expect($li.get()).textContent.'first item'.to.equal('testertalk')
})
可以看到,上述代码共有三个断言,分别是一个.should(?)和两个expect(?)断言 (.and(?)断言实际上是.should(?)断言的别名,它是.should(?)的自定义回调断言,其中包含两个expect(?)断言)
在测试执行过程中,如果第二个断言失败了,第三个断言就永远不会执行,如果导致第二个断言失败的原因被找到且修复了,且此时整个命令还没有超时,那么在进行第三个断言前,会再次重试第一和第二个断言。
重试
Cypress仅会重试那些查询DOM的命令:cy.get( )、.find( )、.contains( )等。你可以通过查看其API文档中的“Assertions”部分来检查是否重试了特定命令。例如,.first( )命令将会一直重试,直到紧跟该命令后的所有断言都通过为止。
表4-5列出了一些常用的可重试命令。
重试的超时时间是4秒,配置项是defaultCommandTimeout,如果想更改自动重试的默认时间,在cypress.json里更改相应字段即可。
测试报告
内置测试报告
内置的测试报告包括Mocha的内置测试报告和直接嵌入在Cypress中的测试报告,主要有以下几种。
? spec格式报告
spec格式是Mocha的内置报告,它的输出是一个嵌套的分级视图。在Cypress中使用spec格式的报告非常简单,你只需要在命令行运行时加上“–reporter=spec”参数即可(请确保你已在package.json文件的scripts模块加入了如下键值对"cypress:run": “cypress run”)。
json格式报告
json测试报告格式将输出一个大的JSON对象。同样的,在Cypress中使用json格式的测试报告,只需要在命令行运行时加上“–reporter=json”参数即可(请确保已在package.json文件的scripts模块加入了键值对"cypress:run": “cypress run”)
junit 格式报告
junit 测试报告格式将输出一个xml文件。在Cypress中使用junit 格式的测试报告,只需要在命令行运行时加上“–reporter=junit” 参数即可(请确保已在package.json文件的scripts模块加入了如下键值对"cypress:run": “cypress run”)。
#进入项目根目录(本例为E:\Cypress) C:\Users\Administrator>E: E:\>cd Cypress #指定reporter为spec E:\Cypress> yarn cypress:run --reporter junit --reporter-options "mochaFile=results/test-output.xml,toConsole=true" 运行完成后,测试报告“test-output.xml”会生成在项目根目录下的results文件夹内,同时console上也会展示,如图4-5所示。
自定义测试报告
用浏览器打开“mochawesome.html”文件,可以看到mochawesome报告,如图4-7所示。
混合测试报告
Cypress除了支持单个测试报告,还支持混合测试报告。用户通常希望看到多个报告,比如测试在CI中运行时,用户既想生成junit格式的报告,又想在测试运行时实时看到测试输出。
Cypress官方推荐使用“mocha-multi-reporters”来生成混合测试报告。使用“mocha-multi-reporters”的步骤如下。
(1)将mocha,mocha-multi-reporters,mocha-junit-reporter添加至你的项目。
#进入项目根目录(本例为E:\Cypress) C:\Users\Administrator>E: E:>cd Cypress #安装mocha,mocha-multi-reporters, mocha-junit-reporter,如已安装则可略过 E:\Cypress>npm install --save-dev mocha@5.2.0 E:\Cypress>npm install mocha-multi-reporters --save-dev E:\Cypress>npm install mocha-junit-reporter --save-dev (2)在E:\Cypress\cypress文件夹下,创建reporter文件夹,并新建一个文件,命名为“custom.json”,增加如下内容。
{ “reporterEnabled”: “spec, json, mocha-junit-reporter”, “reporterOptions”: { “mochaFile”: “cypress/results/iTesting -custom-[hash].xml” } } (3)在E:\Cypress文件下,执行命令:
“yarn cypress:run–reporter mocha-multi-reporters --reporter-options configFile =./reporters/ custom.json”。
#进入项目根目录(本例为E:\Cypress) C:\Users\Administrator>E: E:>cd Cypress #生成mocha-multi-reporters报告 E:\Cypress>yarn cypress:run --reporter mocha-multi-reporters --reporter-options configFile=./reporters/custom.json 运行完成后,测试报告文件夹“results”会生成在项目根目录下,同时,json格式的报告也在运行中显示在console里,如图4-9所示。
图4-9 混合格式测试报告
当用户运行完一次测试(可能包括多个spec),用户希望看到一个完整的测试报告文件,而不是分割开来的独立文件。特别地,对于生成的HTML格式报告来说,用户希望能整合在同一个报告中,Cypress也提供了高阶的方法来满足此需求。
测试用例的组织和编写
Cypress底层依赖于很多优秀的开源测试库,其中就包含Mocha。Mocha是一个适用于Node.js和浏览器的测试框架。它使异步测试变得简单、灵活和有趣。
Mocha还提供了多种接口来定义测试套件,Hooks和单个测试(Individual tests)
BDD(Behavior-Driven Development,行为驱动开发)、TDD(Test- Driven Development、测试驱动开发)、Exports、QUnit和Require。
Cypress采纳了Mocha的BDD语法,该语法非常适合集成测试和单元测试。
Cypress将Mocha硬编码在自己的框架中,在Cypress中,你要编写的所有测试用例都基于Mocha提供的如下基本功能模块:
? describe( )
? context( )
? it( )
? before( )
? beforeEach( )
? afterEach( )
? after( )
? .only( )
? .skip( )
对于一条可执行的测试来说,有以下两个必要的组成部分:
? describe( )
测试套件。可以在里面可以设定context( ),可包括多个测试用例it( ),也可以嵌套测试套件。
? it( )
用于描述测试用例。一个测试套件可以不包括任何钩子函数(Hook),但必须包含至少一个测试用例it( )。
除这两个功能模块外,其他功能模块对于一条可执行的测试来说,都是可选的。例如context( )是describe( )的别名,其行为方式与describe( )相同,使用context( )只是提供一种使测试更易于阅读和组织的方法。
Hook,常被翻译成钩子函数。Mocha提供了如下四种钩子函数。
? before( )
? after( )
? beforeEach( )
? afterEach( )
describe('钩子函数', ()=> {
before(()=> {
after(function( ) {
});
beforeEach(function( ) {
afterEach(function( ) {
});
skip 和 only
排除测试套件/测试用例可使用功能模块.skip( )。
? 排除测试套件describe( )
可以用describe.skip( )来排除无须执行的测试套件
describe.skip('登录', function ( ) {
const username = 'jane.lane'
const password = 'password123'
context('HTML表单登录测试', function ( ) {
it('登录成功,跳转到dashboard页', 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( )
cy.get('h1').should('contain', 'jane.lane') }) }) })
describe('测试1=1', function ( ) {
it('测试1=1', function ( ) {
expect(1).to.equal(1) })
context.skip('排除测试套件',function( ){ it('测试1!=2', function( ){ expect(1).not.to.equal(2) }) }) })
可以看到只有第二个测试套件里的it( )下的测试用例执行了。第一个测试套件和第二个测试套件(context是describe的别名)均没有执行,Cypress标记为未执行。
? 排除测试用例it( )
可以用it.skip( )来排除无须运行的测试用例。
包含测试套件/测试用例可使用功能模块.only( )。需要注意的是,当你用.only( )装饰指定某个测试套件/测试用例时,只有这个测试套件/测试用例会执行,其他未被装饰的测试套件/测试用例不会执行。
? 包含测试套件
可以用describe.only( )来指定要运行的测试套件。
describe.only('登录', function ( ) {
const username = 'jane.lane'
const password = 'password123'
context('HTML表单登录测试', function ( ) {
it('登录成功,跳转到dashboard页', 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( )
cy.get('h1').should('contain', 'jane.lane') }) }) })
describe('测试1=1', function ( ) {
it('测试1=1', function ( ) {
expect(1).to.equal(1) })
context('包含测试套件',function( ){
it('测试1!=2', function( ){
expect(1).not.to.equal(2) }) }) })
可以用it.only( )来指定要运行的测试用例
动态生成测试用例
在实际的项目测试中,有时会碰见多条测试用例执行步骤和检查步骤完全一致,只有输入和输出不同的情况,此时,一条一条地手工编写测试用例的效率就比较低下。下面就来介绍一下如何根据数据动态地生成测试用例。
仍以前面几章使用的例子testLogin.js为例,假设需要登录通过和登录不通过两个测试用例,则动态生成测试用例的步骤如下。
(1)在E:\Cypress\cypress\integration文件夹下,创建一个子目录autoGenTestLogin,在此目录下新建一个testLogin.data.js文件,代码如下:
export const testLoginUser = [ {
summary: "Login pass",
username: "jane.lane",
password: "password123" },
{
summary: "Login fail",
username: "iTesting",
password: "iTesting" } ]
(2)在子目录autoGenTestLogin下,新建一个testLogin.js文件,代码如下:
import { testLoginUser } from '../autoGenTestLogin/testLogin.data'
describe('登录', ()=>{
const username = 'jane.lane'
const password = 'password123'
context('HTML表单登录测试', ()=> {
for(const user of testLoginUser){
it(user.summary, ()=> {
cy.visit('http://localhost:7077/login')
cy.get('input[name=username]').type(user.username)
cy.get('input[name=password]').type(user.password)
cy.get('form').submit( )
cy.get('h1').should('contain', user.username) }) } })
})
然后在Test Runner中选择测试文件夹autoGenTestLogin下的用例“testLogin.js”,单击运行。运行结束后的截图如图5-10所示。
可以看到第一条测试用例执行成功,第二条执行失败了(失败是我们期望的结果),因为用户名和密码不正确,所以无法跳转到dashboard。
根据数据动态生成测试用例,可以提升测试效率,当测试数据本身改变时,无须更改测试代码。
断言
断言是测试用例的必要组成部分。没有断言,用户就无法感知测试用例的有效性。Cypress的断言基于当下流行的Chai断言库,并且增加了对Sinon-Chai,Chai-jQuery断言库的支持。Cypress支持多种风格的断言,其中就包括BDD(expect /should)和TDD(assert)格式的断言。
常见元素的断言有:
? 针对长度(Length)的断言
//重试,直到找到3个匹配的<li.selected>
cy.get('li.selected').should('have.length', 3)
? 针对类(Class)的断言
//重试,直到input元素没有类被disabled为止(或者超时为止)
cy.get('form').find('input').should('not.have.class', 'disabled')
? 针对值(Value)的断言
//重试,直到textarea的值为’iTesting’
cy.get('textarea').should('have.value','iTesting' )
? 针对文本内容(Text Content)的断言
//重试,直到这个spin不包含"click me"字样
cy.get('a').parent('span.help')should('not.contain'.'click me')???
? 针对元素可见与否(Visibility)的断言
//重试,直到这个button是可见为止
cy.get('button').should('be.visible')
? 针对元素存在与否(Existence)的断言
//重试,直到id为loading的spinner不再存在
cy.get('#loading').should('not.exist')
? 针对元素状态(State)的断言
//重试,直到这个radio button是选中的状态
cy.get('.radio').should('be checked')
? 针对CSS的断言
//重试,直到completed这个类有匹配的CSS为止
cy.get('.completed').should('have.css', 'text-decoration', 'line-through')
? 针对回调函数(callback)的断言
假设源HTML如下:
<div class="main-abc123 heading-xyz987">Introduction</div>
如果需要判断类名是否一定含有heading字样,则断言如下:
cy.get('div') .should(($div) => {
expect($div).to.have.length(1)
const className = $div[0].className
expect(className).to.match(/heading-/) })
在具体的使用上,可以按照习惯选择断言库。更多断言库及其用法,请参考如下网址:
https://github.com/chaijs/chai
https://github.com/domenic/sinon-chai
https://github.com/chaijs/chai-jquery
https://www.chaijs.com/api/assert/
观察测试运行
测试运行器(Test Runner[3])是Cypress在一众前端测试框架中脱颖而出的一个重要原因。Cypress使测试在一个独特的交互式运行器中运行测试,使你不仅可以在执行命令时查看这些测试,同时还允许你查看被测应用程序。
Cypress自带的交互式测试运行器功能强大,它甚至允许你在测试运行期间就查看测试命令执行情况,并(同时)监控在命令执行时,被测程序所处的状态。Cypress的测试运行器由如下几个部分组成。
(1)测试状态目录(Test Status Menu)。
测试状态目录用于展示测试用例成功和失败的数目,并且展示每个测试运行的时间。
(2)命令日志(Command Log)。
命令日志用于记录每个被执行的命令。用鼠标单击命令,可在Console中查看命令应用于哪个元素及其执行的详细信息,同时应用程序预览(App Preview) 中会显示当命令执行时被测应用程序的状态。
对于一些特殊的命令例如cy.route( ),cy.stub( )和cy.spy( ),命令日志会展示一个额外的log信息方便你了解当前测试的状态。
(3)URL预览(RUL Preview)。
URL预览用于展示你的测试命令执行时被测应用程序所处的URL,它能够使你更方便地查看测试路由(Testing Routs)。
(4)应用程序预览(App Preview)。
应用程序预览用于展示当测试运行时被测程序所处的实时状态。
(5)视窗大小(ViewPoint Sizing)。
视窗大小可以通过设置视窗大小来测试页面响应式布局。你可以在cypress.json文件中通过配置viewportWidth和viewportHeight两个配置项来控制视窗大小。
(6)Cypress元素定位辅助器(Selector Playground)。
Cypress元素定位辅助器可以帮助用户识别元素唯一的定位标识。
cypress与元素交互
你的每一个测试用例都将包含对元素的操作。健壮、可靠的元素定位策略将是测试成功的保障。Cypress的多种定位策略能够使你聚焦在和元素的交互上而无须过多担心因定位而导致的测试失败。
相对于其他测试框架来说,Cypress有着独一无二的定位策略,能够使你摆脱元素定位的噩梦。在你以往的测试中,一定遇见过以下类似问题。
(1)应用元素ID或者类是动态生成的。
(2)你使用了CSS定位策略,但在开发过程中CSS样式发生了改变。
这种情况下通常测试会失败。
为解决这个问题,Cypress提供了data-*属性。data-*属性包含如下3个定位器:
? data-cy
? data-test
? data-testid
它们都是Cypress专有的定位器,仅用来测试。data-*属性与元素的行为或样式无关,这意味着即使CSS样式或JS行为改变也不会导致测试失败。
举例来说,你可以为button添加如下属性:
html属性 html css html元素
<button id="main" class="btn" data-cy="submit">Submit</button>
<button id="main" class="btn" data-test="submit">Submit</button>
<button id="main" class="btn" data-testid="submit">Submit</button>
在测试用例中,采用如下方法与元素交互:
cy.get('[data-cy=submit]').click( )
cy.get('[data-test=submit]').click()
cy.get('[data-testid=submit]').click( )
除了Cypress专有选择器外,还可以利用以下常规选择器来定位元素。
? #id选择器
#id选择器通过HTML元素的id属性选取指定的元素。
cy.get('#main').click()
class类选择器
类选择器通过HTML元素的class属性选取指定的元素。
cy.get('.bin').click()
? attributes属性选择器
属性选择器通过HTML元素的属性选取指定的元素。
cy.get('button[id = "main"]').click()
:nth-child(n) 选择器
:nth-child(n) 选择器匹配属于其父元素的第n个子元素,不论元素的类型。
<ul>
<li> iTesting </li>
<li>Ray</li>
<li>Kevin</li>
<li>Emily</li>
</ul>
cy.get('li:nth-child(1)').click( )
Cypress.$定位器
针对难以用普通方式定位的元素,Cypress还允许使用jQuery选择器Cypress.$(selector) 直接定位。
//Cypress查找元素,selector使用id Cypress.$(’#main’) //等同于 cy.get(’#main’)
cypress于页面元素相互交互
查找页面元素基本方法
<ul>
<li id=”id”>iTesting</li>
<li>Ray</li>
<li>Kevin</li>
<li>Emily</li>
</ul>
.find(selector)方法用来在DOM树中搜索被定位的元素的后代,并用匹配元素来构造一个新的jQuery对象。
.find(selector)的语法如下:
.find(selector) .find(selector)的用法如下:
cy.get('ul').find('#id')
cy.find('#id')
.get(selector)
.get(selector)方法用来在DOM树中查找selector对应的元素。
.get(selector)的语法如下:
.get(selector)的用法如下:
cy.get('#id')
.contains(selector)
.contains(selector)方法用来获取包含文本的DOM元素。
.contains(selector)的语法如下:
.contains(content)
.contains(selector, content)
.contains (selector)的用法如下:
cy.contains('iTesting')
cy.contains('li','iTesting')
cy.contains(/^i\w+/)
查找页面元素辅助办法
由于现代Web应用程序比较复杂,单一的定位方法往往不能精准地定位到所需元素,Cypress提供了一些辅助方法,可以提高查找元素的准确性。以下是一些常用的辅助方法。
假设存在DOM树如下:
<ul>
<li id=”id”>iTesting</li>
<li>Ray</li>
<li id= ”kevin”>Kevin</li>
<li>Emily</li>
</ul>
.children( )
.children ( )方法用来获取DOM元素的子元素。
.children ( )的语法如下:
.children( ) .children(selector) .children ( )的用法如下:
cy.get('ul').children()
cy.get('ul').children('#id')
.parents( )
.parents( )方法用来获取DOM元素的所有父元素。
.parents( )的语法如下:
.parents( ) .parents(selector) .parents( )的用法如下:
cy.get('#id').parents()
.parent( )
与.parents( )命令相反,.parent( )仅沿DOM树向上移动一个级别,它获得的是指定DOM元素的第一层父元素。
.parent( )的语法如下:
.parent( ) .parent(selector) .parent( )的用法如下:
cy.get('#id').parent( )
.siblings( )
.siblings( )方法用来获取DOM元素的所有同级元素。
.siblings( )的语法如下:
.siblings( ) .siblings( )(selector) .siblings ( )的用法如下:
cy.get('#id').siblings( )
.first( )
.first ( )方法用来匹配给定DOM对象集的第一个元素。
.first ( )的语法如下:
.first( ) .first ( )的用法如下:
cy.get('#id').first( )
.last ( )方法用来匹配给定DOM对象集的最后一个元素。
.last ( )的语法如下:
.last( ) .last ( )的用法如下:
cy.get('ul').last( )
.next( )
.next ( )方法用来匹配给定DOM对象紧跟着的下一个同级元素。
.next ( )的语法如下:
.next( ) .next ( )的用法如下:
cy.get('ul').next( )
? .nextAll( )
.nextAll ( )方法用来匹配给定DOM对象之后的所有同级元素。
.nextAll ( )的语法如下:
.nextAll( ) .next ( )的用法如下:
cy.get('#id').nextAll( )
? .nextUntil(selector)
.nextUntil( )用来匹配给定DOM对象之后的所有同级元素直到遇到Until里定义的元素为止。
.nextUntil ( )的语法如下:
.nextUntil(selector) .nextUntil(selector, filter) .nextUntil ( )的用法如下:
cy.get('#id').nextUntil('#kevin')
? .prev( )
.prev( )方法用来匹配给定DOM对象紧跟着的上一个同级元素。
.prev( )的语法如下:
.prev( ) .prev( )的用法如下:
cy.get('ul').prev( )
? .prevAll( )
.prevAll( )方法用来匹配给定DOM对象之前的所有同级元素。
.prevAll( )的语法如下:
.prevAll( ) .prevAll ( )的用法如下:
cy.get('#id').prevAll( )
? .prevUntil( )
.prevUntil( )用来匹配给定DOM对象之后的所有同级元素直到遇到Until里定义的元素为止。
.prevUntil( )的语法如下:
.prevUntil(selector) .prevUntil(selector, filter) .prevUntil( )的用法如下:
cy.get('#kevin').prevUntil('#id')
? .each( )
.each( )用来遍历数组及其类似结构(数组或对象有length属性)。
.each( )的语法如下:
.each(callbackFn) .each( )的用法如下:
cy.get('#ul').each(($li)=>{ cy.log($li.text( )) })
? .eq( )
.eq( )用来在元素或者数组中的特定索引处获取DOM元素。它的作用跟jQuery中的:nth-child( )选择器相同。
.eq( )的语法如下:
.eq(index) .eq( )的用法如下:
//获取ul的第一个字元素
cy.get('#ul').eq(0)
对元素操作——click(),type(),clear(),check(),uncheck(),trigger(),select()
.click( )
单击某个元素。.click( )的语法如下:
.click( )
.click(options)
.click(position)
其中,options可选参数包含{force:true}和{multiple:true}。
cy.get('li').click({ force: true })
cy.get('li').click({ multiple: true })
有时候需要对某个元素的某个具体位置进行单击,click也提供了相应的方法。
cy.get('li').click({'topRight'})
cy.get('li').click({'topLeft'})
cy.get('li').click({'top'})
cy.get('li').click({'left'})
cy.get('li').click({'center'})
cy.get('li').click({'right'})
cy.get('li').click({'bottomLeft'})
cy.get('li').click({'bottom'})
cy.get('li').click({'bottomRight'})
.click( )还可以接受键值组合,例如“Shift+click”。
cy.get('body').type('{shift}', { release: false })
cy.get('li:first').click( )
除Shift外,.click( )还支持如下按键:
{alt}:按住Alt键。
{ctrl}:按住Ctrl键。
? .dblclick( )
双击某个元素。.dblclick( )的语法如下:
.dblclick( ) /
/带参数的双击
.dblclick(options)
.dblclick(position)
其中,options参数和position参数的选项跟.click( )完全一致。
? .rightclick( )
右击某个元素。.rightclick( )的语法如下:
.rightclick( )
.rightclick(options)
.rightclick(position)
其中,options参数和position参数的选项跟.click( )完全一致。
? .type( )
往DOM元素中输入。.type( )的语法如下:
.type(text, options)
例如:
cy.get('input[username=”name”]').type('iTesting')
在日常测试过程中,若需要输入一些特殊字符,text参数可以使用下列文本:
text参数支持的其他特殊字符如下:
{backspace}:删除光标左侧的字符;
{del}:删除光标右侧的字符;
{downarrow}:向下移动光标;
{end}:将光标移到行尾;
{enter}:按Enter键;
{esc}:按ESC键;
{home}:将光标移到行首;
{insert}:在光标右边插入字符;
{leftarrow}:向左移动光标;
{pagedown}:向下滚动;
{pageup}:向上滚动;
{rightarrow}:向右移动光标;
{selectall}:通过创建选择范围来选择所有文本;
{uparrow}:向上移动光标。
Options可接受如下参数:
? .clear( )
.clear( )清除输入或文本区域的值。.clear( ) 语法如下:
.clear( ) 例如:
cy.get('input[username=”name”]').clear( )
cy.get('input').type({selectall}{backspace})
? .check( )
针对类型的单选框(radio button) 或者复选框(check box) ,Cypress提供了check和uncheck方法直接操作。语法如下:
//选中 .check( ) //选中一个选项,值是value .check(value) //选中多个选项 .check(values) 例如:
cy.get('[type="radio"]').check('US')
cy.get('[type="checkbox"]').check(['ga', 'ca'])
? .uncheck( )
.uncheck( )跟.check( )的用法相反,它用于取消选中单选框或者复选框。语法如下:
例如:
cy.get('[type="radio"]').uncheck('US')
cy.get('[type="checkbox"]').uncheck(['ga', 'ca'])
? .select( )
.select( )用来在中选择一个。语法如下:
.select(value) .select(values) 假设DOM树如下所示:
<select>
<option value="1">iTesting</option>
<option value="2">kevin</option>
<option value="3">emily</option>
</select>
select( )写法如下:
cy.get('select').select('iTesting')
cy.get('select').select(['iTesting', 'Kevin'])
? .trigger( )
.trigger( )用来在DOM元素上触发事件。语法如下:
.trigger(eventName) 例如:
cy.get('button').trigger('mousedown')
cy.get('button').trigger('mouseover') /
/抬起光标 cy.get('button').trigger('mouseleave')
cypress常见操作
Cypress中有如下几种常见的操作场景。
? 访问某个网站
cy.visit('https://helloqa.com')
如果你在cypress.json中配置了baseUrl的值,则Cypress将自动为你加上前缀。
{ "baseUrl": "http://www.helloqa.com" }
cy.visit('/categories/api-test')
? 获取当前页面URL地址
在Cypress中,可以使用下述方式来获取当前页面地址。
cy.url().should('contains','api-test')
? 刷新当前页面
在Cypress中,可以使用cy.reload( )来刷新当前页面。
cy.reload( )
CTRL+ F5
cy.reload(true)
? 最大化窗口[1]
在Cypress中,默认运行时的窗口大小为1000px1660px。如果你的屏幕不够大,无法显示完整的像素,Cypress将自动缩小并居中显示你的应用程序。可以通过以下两种设置来设置运行窗口。
- 在cypress.json中设置
{ "viewportWidth": 1000, "viewportHeight": 660 }
- 在运行中设置
//运行中设置 cy.viewpoint(1024, 768) ? 网页的前进或后退
在Cypress中使用cy.go( )来实现网页的前进或后退。前进或后退的依据是浏览历史记录中的URL。
cy.go('back')
cy.go(-1)
cy.go('forward')
cy.go(1)
? 判断元素是否可见
在Cypress里,要判断元素是否可见,可以直接使用should判断,Cypress会自动为你重试直至元素可见或者超时。
cy.get('.check-box ').should('be.visible')
cy.get('.check-box').should('be.visible')
? 判断元素是否存在
cy.get('.check-box').should('exist')
cy.get('.check-box').should('not.exist')
? 条件判断
在日常测试中,有时候需要对某个元素进行条件判断,即满足条件A时执行A操作,满足条件B时执行B操作。Cypress称之为“条件测试(Conditional Testing)” 并建议避免编写包含条件测试的脚本,因为条件测试通常比较脆弱,容易导致测试失败。
一个典型的例子是如果元素A存在,则执行单击A操作,如果不存在,则什么都不做。
Cypress建议脚本编写者提供A条件出现的必要步骤来确保A条件一定会满足从而避免条件判断。
但你仍然可以用Cypress支持jQuery的特性来使用条件判断。
const btnLocator = '#btn'
Cypress.$(btnLocator).length>0){
cy.get(btnLocator).click( )
}
? 获取元素属性值
在Cypress中,无法直接返回元素属性值。
cy.get('#btn').then(($btn) => {
const btntxt = $btn.text()
cy.log(btntxt)
})
? 清除文本
在Cypress中,可使用cy.clear( )来清除input输入框和textarea输入框的值。
//清除input输入框的值 cy.get('input[name=username]').clear( ) //等同于 cy.get('input[name=username]').type({selectall} {backspace}) ? 操作表单输入框
在Cypress中,可使用cy.clear( )和cy.type( )组合来操作输入框。
//清除 username 并输入用户名“iTesting” cy.clear(‘input[name=username]’).type(‘iTesting’) ? 操作单选/多选按钮
针对类型的单选框或者复选框,Cypress提供了check和uncheck方法直接操作。
cy.get('[type="radio"]').check('US')
cy.get('[type="radio"]').uncheck('US')
? 操作下拉菜单
如果下拉菜单是Select形式的,则直接使用如下方式操作。
cy.get('select').select('下拉选项的值')
如果下来菜单是其他形式的,例如DOM树形结构如下所示:
<div id="form">
<ul role="listbox" class="select-dropdown-menu">
<li role="option" class="select-dropdown-item">iTesting</li>
<li role="option" class="select-dropdown-item">kevin</li>
<li role="option" class="select-dropdown-item">emily</li>
</ul> </div>
则查找iTesting并选中的写法如下:
cy.get('li').eq(0).click( )
? 操作弹出框
最常见的是提交确认弹出框,解决方法跟正常的页面一样,首先使用cy.get( )或cy.find( )定位到弹出框元素,然后操作即可。
对于iframe格式的弹出框,可以通过闭包解决。
cy.get('iframe') .then(function ($iframe) {
const $body = $iframe.contents( ).find('body')
cy.wrap($body).find('#btn').click( ) })
? 操作被覆盖元素
碰见元素被覆盖无法操作的情况,可以直接使用{force:true}。
//强制单击 btn 元素 cy.get('#btn').click({ force: true }) ? 操作页面滚动条
滚动条操作有两种方式,一种是元素不在视图中,需要拖动滚动条直到元素出现,如下图所示。
假设DOM树如下所示:
<div id="scroll-horizontal" style="height: 300px; width: 300px; overflow: auto;">
<div style="width: 1000px; position: relative;"> 水平滚动框 <button class="btn btn-danger" style="position: absolute; top: 0; left: 500px;">提交<
/button> </div> </div>
由DOM结构得知,滚动框的视图宽度只有300px,但要操作的“提交”button却在1000px处。操作此按钮的代码如下:
cy.get('#scroll-horizontal button') .should('not.be.visible')
y.get('#scroll-horizontal button').scrollIntoView( ) .should('be.visible')
cy.get('.btn btn-danger').click( )`
滚动条有时也可作为操作设置某项属性出现,如图6-2所示。
模拟键盘
模拟键盘操作
模拟键盘操作,例如按Enter键等。
cy.get('input["id=mail"]').type('fake@email.com')
cy.get('input["id=mail"]').type('{enter}')
更多关于模拟键盘的操作,请参考.type( )和.click( )的参数。
测试运行器
元素定位策略
Cypress在定位元素时会遵循以下的优先级:
(1)data-cy;
(2)data-test;
(3)data-testid;
(4)id;
(5)class;
(6)tag;
(7)attributes;
(8)nth-child。
Cypress会尝试从高优先级的定位策略开始,定位目标元素。如果默认的定位顺序不符合应用程序实际情况,你可以更改元素定位的优先级顺序。语法如下:
Cypress.SelectorPlayground.defaults(options)
Cypress.SelectorPlayground.getSelector($el)
其中options的可选值是以上八种定位策略的一种或多种:
//设置定位策略优先级,最高级是id
Cypress.SelectorPlayground.defaults({ selectorPriority: ['id', 'class', 'attributes'] })
例如,假设有如下HTML代码段:
登录 默认情况下,获取到的元素选择器的值应该是login。
const $el = Cypress.$('button')
const selector = Cypress.SelectorPlayground.getSelector($el)
更改元素定位策略,再次获取元素选择器值,selector的值变成了“login-class”。
Cypress.SelectorPlayground.defaults({ selectorPriority: ['class', 'id'] })
const $el = Cypress.$('button') const selector = Cypress.SelectorPlayground.getSelector($el)
时间穿梭
如果测试过程中发生错误,大部分的测试框架都无法得知测试执行时被测应用程序所处的状态,只能在测试运行结束后通过日志、截图来猜测测试失败的原因。Cypress测试运行器则完全相反。它忠实地记录了每一条测试命令执行时被测应用程序所处的状态,并且保存起来以便随时回溯,这种能力被称为时间穿梭(time-travel)。
需要注意的是,Cypress保存的是应用程序状态,不是截图。故Cypress支持查看命令执行时发生的一切操作,用户可直接定位到错误的根本原因,无须猜测。
在测试结束后,可以通过鼠标悬停,或者用鼠标单击某个命令的方式来进行时间穿梭。
使用鼠标悬停,可以在应用程序预览中查看命令作用到被测应用的具体情况。
使用鼠标单击,将在浏览器的Console中看到当命令执行时,应用到了被测应用程序的哪个元素上,以及当时的上下文详细信息。
测试习惯
Cypress命令是异步的
初次使用Cypress,你产生的第一个问题恐怕就是为什么元素赋值不成功。
我们来通过一段代码来解释。假设你使用的是Selenium。
driver = webdriver.Chrome( )
driver.get('http://www.helloqa.com')
Ids = driver.find_element_by_id('id')
Ids.find_element_by_id('id2').click( )
如果你使用Cypress来“翻译”上述代码,则会出错。
const Ids = cy.get("#id")
失败的原因是Cypress命令不是同步执行的。Cypress命令在被调用时并不会被马上执行,Cypress会先把所有命令排队(enquene),然后再执行。也就是说当cy.get("#id")被初次调用时,Ids的值是undefined,故测试会失败。
箭头函数(慎用)
var x = function(x, y) { return x * y; }
const x = (x, y) => x * y;
但在Cypress中,使用箭头函数时需注意:
describe('测试箭头函数', function ( ) {
beforeEach(function ( ) {
cy.wrap('hello').as('text') })
it('访问不到', ( )=> {
cy.log(this.text) }) })
cypress中赋值永远失败
同源策略
同源策略是浏览器安全的基石。这也意味着当两个iframe直接有访问时,必须同时满足协议相同、域名相同、端口相同三个条件。由于Cypress是运行在浏览器之中的,要测试应用程序,Cypress必须始终能够和应用程序直接通信,但显然浏览器的同源策略不允许。
Cypress通过以下方式“绕过了”浏览器的限制。
(1)将document.domain注入text / html页面。
(2)代理所有HTTP / HTTPS通信。
(3)更改托管的URL以匹配被测应用程序的URL。
(4)使用浏览器的内部API进行网络间通信。
首次加载Cypress时,内部Cypress Web应用程序托管在一个随机端口上,类似于http://localhost:65874 / __ /。
在一次测试中,当第一个cy.visit( )命令被发出后,Cypress将更改其URL以匹配远程应用程序的来源,从而解决了同源策略的主要障碍。但这样带来的坏处是在一次测试运行中,访问的域名必须处于同一个超域(Super domain)下,否则Cypress测试将会报错。
describe('一次测试访问不同域名', function ( ) {
let testVar it('立刻报错', function ( ) {
cy.visit('https://helloqa.com')
cy.visit('https://www.baidu.com') }) })
.then()与闭包
在Cypress中,保存一个值或者引用的最好方式是使用闭包。.then( )是Cypress对闭包的一个典型应用。.then( )返回的是上一个命令的结果,并将其注入下一个命令中。
举例来说,获取button文本改变前后的值并用于比较。
cy.get('button').then(($btn) => {
const txt = $btn.text( )
cy.get('form').submit( )
cy.get('button').should(($btn2) => {
expect($btn2.text( )).not.to.eq(txt) }) })
.wrap()与.as()
考虑这样一种情形,当你的测试需要一个前置条件才能执行,比如得到数据库的某个表的值。那么,该如何做呢?
/这种方法可以实现,但是不够优雅
describe('a suite', function ( ) {
let text beforeEach(function ( ) {
cy.visit('http://www.helloqa.com')
cy.contains('首页').then(($el)=>{
text = $el.text( ) }) })
it('does have access to text', function ( ) {
cy.log(text) }) })
.wrap( )返回传递给它的对象。语法如下:
cy.wrap(subject)
cy.wrap(subject, options)
const getName = ( ) => {
return 'iTesting' }
cy.wrap({ name: getName }).invoke('name').should('eq', 'iTesting')
.as( )用于分配别名以供以后使用。稍后在带有@前缀的cy.get( )或cy.wait( )命令中引用该别名。
.as(aliasName)
describe('a suite', function ( ) {
beforeEach(function ( ) {
cy.visit('http://www.helloqa.com')
cy.contains('首页').then(($el)=>{
text cy.wrap($el.text( )).as('text')
}) })
it('does have access to text', function ( ) {
cy.get('@text').then((el)=>{
cy.log(el)
})
}) })
cypress实战
|