在我们写端到端测试之前,我们应该明确我们是基于一个用户的角度去测试我们的页面,所以这无关我们的所有源码,我们应该只专注于浏览器所呈现给我们的资源,包括页面上的element
、控制台中network
中的所有的请求以及导航栏上的url
信息,这是我们可以去测试和观察到的所有的点。
// 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()
访问一次页面就可以看到我们所需要被模拟的请求已经被存根并且成功模拟了。当然我们的网页不仅仅只有一些点击事件,我们通常还有很多特殊的操作,比如拖拽以及文件的上传等等。这里我讲解一下我遇到过的文件上传的模拟问题。
例如我们有一个这样的场景
我图中的这个按钮中我们所使用的是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文件
3. 最后我们来展示一下上传文件的代码段
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`)
})
我们运行一下测试用例并且观看一下快照库
到这一步为止可以看到我们的文件已经上传成功了并且文件名已经被成功渲染到了页面上。