前端自动化测试概述
- 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', => (){
//一个visit命令
cy.visit("http://localhost:7077/login")
//一个get命令,一个type命令
cy.get('input[name=username]').type(username)
//一个get命令,一个type命令
cy.get('input[name = password]').type(password)
//一个get命令,一个submit命令
cy.get('form').submit()
//一个url命令,一个断言
cy.url().should('include','/dashboard')
//一个get命令,一个断言
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) =>{
//更多断言
//期望下拉列表的第一个选项的textContent是‘iTesting’
expect($li.get()).textContent.'first item').to.equal('iTesting')
//期望下拉列表的第二个选项的textContent是‘testertalk’
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:\