简介
cypress 是一个端到端的测试框架,其中的两个"端"分别指的是前端和后端。也就是说,cypress 不仅可以测试前端代码,还能测试接口。但实际使用中,大部分的工作都不会也不应该调用真正的后端接口。而是采用 mock 的方式替代,类似于jest.spy()
那种方式去处理接口请求。
Install
lua
$ npm install cypress --save-dev
$ npx cypress open
目录
安装完成之后会在根目录下自动生成 cypress 文件夹和 cypress.config.ts 配置文件。
其中 downloads 是测试中下载内容的存放位置
e2e 是存放测试用例的地方
fixtures 是存放各种固定数据的地方
support 是存放一些调整测试环境的脚本的地方
Config
默认情况下,任何报错都会导致测试中止。但对于大型的项目来说,可能出错的地方太多。而我们通常希望测试的范围是很小的。所以建议关掉 Cypress 对这类出错的监控
js
Cypress.on('uncaught:exception', (err, runnable) => {
return false;
})
写一个测试用例
首先需要打开 cypress 的客户端。
arduino
$ npx cypress open
当然,你可以把它写在 script 中,改一个你喜欢的命令名字。
我们选择 E2E Testing,如果是首次进入的话,这里会为你自动生成一些默认的配置文件,直接下一步就行。
选择运行环境,这里我们选 chrome。然后点击 「Start E2E Testing in Chrome」。
之后 cypress 会打开一个新的浏览器。
然后直接点击 spec.cy.ts 就可以开始跑测试用例了。
测试用例
cypress 的测试用例的整体结构和其他测试框架都是类似的,都是 describe + it 组成。
js
describe('My First Test', () => {
it('clicks the link "type"', () => {
cy.visit('https://example.cypress.io')
cy.contains('type').click()
})
})
比较不用的是 cypress 的 API。cypress 的 api 按照 jquery 的链式调用设计,用起来很方便,很容易上手。
一个普通的测试用例基本上由四部分构成:
- 访问一个路由
值得一提的是,「访问一个路由」可以是访问任意一个路由。并不一定非得是自己的本地项目。
- 搜索页面上的某个元素
cypress 和其他流行的测试库一样,除了根据 css 选择器获取元素,也支持根据文字获取元素。
jsx
<Button>查询</Button>
//antd 的按钮会自动在文字中间加空格,所以我们也加上。
cy.contains('查 询');
jsx
<form>
<div>
<label>name</label>
<input name="name" />
</div>
<div>
<label>age</label>
<input name="age" />
</div>
<input type="submit" value="submit the form!" />
</form>
// 利用 get 缩小搜索范围
cy.get('form').contains('submit the form!').click()
- 与这个元素进行某种交互
最常用的就是点击和键盘输入,这两个操作都有现成的 api 可用
js
// click
cy.get('.btn').click() // Click on button
cy.focused().click() // Click on el with focus
cy.contains('Welcome').click() // Click on first el containing 'Welcome'
// type
cy.get('input').type('Hello, World') // Type 'Hello, World' into the 'input'
- 断言
cypress 封装了 should 和 and 方法,可以在保持链式调用的同时做出断言。
js
cy.get('.error').should('be.empty') // Assert that '.error' is empty
cy.contains('Login').should('be.visible') // Assert that el is visible
cy.wrap({ foo: 'bar' }).its('foo').should('eq', 'bar') // Assert the 'foo' property equals 'bar'
stub
cypress 是一个"端到端"的测试框架,所以除了前端逻辑之外,另一个重点就是接口的测试。
cypress 提供 cy.intercept()
方法拦截、mock 接口,官方管这个操作叫做"stub",我也不知道该怎么翻译。
stub 这个词一般指的是一个物体剔除掉大部分之后剩下的那部分,比如一根烟抽到最后剩下的烟屁股,就可以叫做这根烟的 "stub";再比如一个树的树桩;铅笔用到最后剩下的铅笔头等等。
引申意思可以指一张票的票根。
我暂且把 stub 翻译成"模拟"
引入请求之后,我们才算得上真正测试一个前后端分离的项目。
cypress 几乎能模拟接口的所有参数,包括强制增加接口的延迟,而且不需要修改源码,速度也很快。
这样我们就可以方便的模拟接口的各种行为,比如接口报错,某个字段为空等常见错误。
js
// any request to "/search/*" endpoint will
// automatically receive an array with two book objects
cy.intercept('/search/*', [{ item: 'Book 1' }, { item: 'Book 2' }]).as(
'getSearch'
)
cy.get('[data-testid="autocomplete"]').type('Book')
// this yields us the interception cycle object
// which includes fields for the request and response
cy.wait('@getSearch')
.its('request.url')
.should('include', '/search?query=Book')
cy.get('[data-testid="results"]')
.should('contain', 'Book 1')
.and('contain', 'Book 2')
理论上,我们还可以用真实的接口去跑测试用例,但并不推荐这样做。
一个原因是,真实接口不够灵活,没法随意调整更改,况且我们还想测试接口报错这样的场景。
另一方面,预发接口通常不够稳定,也不够快速。用这样的接口有时单纯的开发都有点难受,更别提拿它跑用例了。
但这倒不是说就完全抛弃真实接口,我们当然可以等开发后期,大部分功能都已稳定的时候,再用真实的接口跑一遍用例。
测试思路
知道了如何写测试用例,基本上就可以把 cypress 用起来了,不得不说 cypress 的测试驱动开发的开发体验还不错。
相信大家平常写的较多的还是组件的测试用例,这跟 cypress 提倡的端到端的测试不太一样。
一般组件的测试用例都是针对当前组件的各种边界情况,每个 api 的功能等等。
比如一个日期选择器的 value 属性,如果它的值不合法怎么办?如果它超出了给定的日期范围怎么办?如果不传 value 怎么办?组件是否重新渲染了太多次?等等。都是围绕组件本身展开的。
但在 cypress 的 e2e 测试中,你所面对的是一个完整的应用。
你采取的视角更接近一个真实的测试同学的视角。比如,测试每个筛选项是否生效?表单的校验是否正确?数据的增删改查是否正常?等等。
你的测试用例写起来的过程也更像一个真实的用户的操作流程:
访问页面 => 选择元素 => 与这个元素交互 => 期待应用发生某个行为
总结
cypress 是个不错的端到端的测试框架,提供了不错的 api 和测试环境。但在真实的开发中,引入测试用例还需要考虑很多其他的问题
比如:
- 编写测试用例的成本?测试用例本质上也是一段代码,也需要精力去编写和调试。是否能找到一个稳定且快速的编写测试用例的方法?
- 业务需求是否经常变动?如果业务需求经常变动,那之前编写的测试用例可能都需要重新调试。
- 测试用例的覆盖率?因为是端到端的测试,所以边界情况非常多,为了减少编写测试用例的成本,可能需要判断下,哪些场景需要测试,哪些不需要
综上,对于提升代码质量这个问题,使用测试框架确实能一定程度上的提高代码质量,但在实际工作上,我们需要更全面的根据实际情况考虑这个问题。