前言
在我们写端到端测试之前,我们应该明确我们是基于一个用户的角度去测试我们的页面,所以这无关我们的所有源码,我们应该只专注于浏览器所呈现给我们的资源,包括页面上的element、控制台中network中的所有的请求以及导航栏上的url信息,这是我们可以去测试和观察到的所有的点。
语法实战
spec基本结构
// https://docs.cypress.io/api/introduction/api.html import { DEV_SERVER } from '../config/conf' describe('主页', () => { it('Home', () => { cy.visit('/') cy.contains('h1', 'QDeploy 智能安装部署平台') cy.get('button').click() cy.url().should('eq', `${DEV_SERVER}steps/selectMode`) }) })
这里举一个最简单的例子,和单元测试一样,首先要把所有的用例包裹在一个describe中。在用例中先用cy.visit()方法访问地址,这里后面只加了/是因为baseUrl已经设置过了的原因。
使用cy.contains()或者cy.get()去抓取DOM并进行断言,Cypress中默认包含的断言库为Chai。由于设有异步等待的机制,所以我们可以毫无顾及地去写下一步的操作,包括button的点击事件和跳转之后url的判断。
生命周期
在一个测试集合中,我们也可以加入自身的生命周期,这些生命周期主要是针对每个测试用例来执行的,包括beforeEach、beforeAll、afterEach、afterAll。我在这个测试集合中主要用到了beforeEach这个声明周期,在每个测试用例开始之前我都对我需要的DOM进行抓取并取一个别名,这样我方便其他用例需要时就不需要再反复去寻找这个节点对象了。
beforeEach(() => { cy.visit('/#/steps/selectMode') cy.get('.one__item__right').eq(0).find('.item__right__btn').eq(0).as('hasConfigFile') cy.get('.one__item__right').eq(0).find('.item__right__btn').eq(1).as('notConfigFile') cy.get('.one__item__right').eq(1).find('.item__right__btn').eq(0).as('hasSystem') cy.get('.one__item__right').eq(1).find('.item__right__btn').eq(1).as('notSystem') cy.get('.btn__next').as('next') cy.get('.item__upload__text').as('fileName') })
在取了别名之后其他用例只需要调用cy.get('@name')就可以取到相应别名的DOM元素。
模拟请求
在我们测试的时候总是会免不了一些请求的发出,在Cypress中由于是真实的浏览器环境,所以所有的请求都会被正常发出,但是有些时候我们需要mock掉一些请求来观察DOM的反馈是否符合预期,这里就需要引入一个比较重要的概念——存根stub。
不同于单元测试的mock,我认为在单元测试中更类似于axios中的拦截器,对整个请求的代码层面进行一个拦截后返回一个相同格式的对象骗过,而在端到端测试中因为我们无法对项目本身的源码下手,所以我们只能从浏览器层面去模拟,在这里的存根我的理解是在页面发出请求之前,先对一个API做一个标记,当浏览器触发这个方法并发送请求后使用标记后的模拟请求返回并进行后续的断言操作,我们来看一下代码。
describe('installSystem', () => { it('寻找节点失败', () => { cy.server() cy.route({ method: 'DELETE', url: 'api/find/node', status: 200, response: { data: {}, error_code: 1, message: 'fuck' } }) cy.visit('/#/steps/installSystem') cy.wait(1000) cy.get('.pop_content_confirm').find('div').find('div').contains('寻找节点出错') }) })
在这个例子中,由于请求在页面刚被挂载后就被触发了,也就是说整个请求是写在mounted这个声明周期中的,所以我们需要在访问页面之前就对这个需要被mock的api做一个stub。
首先我们使用cy.server()声明一个mock的请求。
然后使用cy.route()去描述我们需要模拟的api的具体信息,在里面可以填写很多的配置,包括请求的方法method,请求的地址url,请求返回的状态码status以及最后返回的response body,在这里由于项目本身中还定义了error_code状态码,所以对于这一个请求所具备的状态我们就需要写很多个测试用例的组合去断言是否符合我们的预期。
然后因为请求已经被我们存根了,我们再去使用cy.visit()访问一次页面就可以看到我们所需要被模拟的请求已经被存根并且成功模拟了。
到目前为止我们就已经非常成功地对一个API进行模拟请求了,对比起单元测试还是方便了不少的。
文件上传
当然我们的网页不仅仅只有一些点击事件,我们通常还有很多特殊的操作,比如拖拽以及文件的上传等等。这里我讲解一下我遇到过的文件上传的模拟问题。
例如我们有一个这样的场景:
我图中的这个按钮中我们所使用的是input [type="file"]这个原生的输入框,所以我们无法通过value本身来获取文件并去模拟,我们需要模拟整个真实的上传操作,而显然在我们点击按钮并选择我们本地的文件是Cypress所无法做到的,毕竟不是外挂。所以我们需要自定义一条命令去完成这一步操作,这里我参考了github中Cypress官方下的一个issue,详见Adding Ability to Submit File to Input Element From Local Filesystem #170。
7. 首先我们需要去tests -> e2e -> support -> commands.js中添加一条自定义的指令:
// 上传文件命令 Cypress.Commands.add('upload_file', (fileName, selector) => { cy.get(selector).then(subject => { cy.fixture(fileName).then((content) => { const el = subject[0] const testFile = new File([content], fileName) const dataTransfer = new DataTransfer() dataTransfer.items.add(testFile) el.files = dataTransfer.files }) }) })
需要声明的是我们在这个文件中我们不仅仅可以自定义指令,并且还可以更改已经存在的api,这些在文件被创建时会在开头有备注说明,这里就不展示了。
8. 然后我们需要去tests -> e2e -> fixtures中添加我们需要上传的文件,我这里准备了一个Excel文件:
最后我们来展示一下上传文件的代码段:
it('上传文件选择有安装系统', () => { cy.upload_file('test.xlsx', 'input[type=file]') cy.get('@fileName').contains('test.xlsx') cy.get('@next').should('not.be.disabled') cy.get('@hasSystem').eq(0).click() cy.get('@next').click() cy.get('.pop_tool').find('button').eq(1).click() cy.url().should('eq', `${DEV_SERVER}steps/createClusters`) })
我们运行一下测试用例并且观看一下快照库:
到这一步为止可以看到我们的文件已经上传成功了并且文件名已经被成功渲染到了页面上。
作者:钟大灵