关于前端测试的一些理论与基于 Cypress
的 E2E 测试具体实践。javascript
平常业务项目开发的痛点之一即是前端的回归测试
,免不了各类手动点点点,但凡改动了某个公用组件,函数,都要漫山遍野地把项目的主要页面都点进去看一遍有没有问题。项目用了 GraphQL
的话,Schema 一个更新不及时,某个没注意到的页面就挂了,而后就等着开 issue 或者报线上 Bug 吧 😐html
经过人工手动点点点不只是累,也并不靠谱,无法保证每一次都测到了须要回归测试的功能。想解决这一痛点,就不得不提前端的自动化测试
。经过命令行跑测试,集成 CI 自动测试岂不美滋滋。前端
然而,国内各厂对于前端自动化测试还没有造成很好的实践,提及自动化测试,你们想到的也仍是后端测试。几回技术大会(JSConf、GMTC 等等)里关于前端测试的话题也是寥寥无几,有的话也是国外前端工程师的分享,或是 QA 关于搭建测试平台的分享。java
在个人经验里,对于业务项目而言的前端测试都很“尴尬”。若是是开发工具库,那能够经过单元测试来保证质量,若是是开发 UI 组件库,能够经过 Storybook 来进行视觉与快照测试。因此以前往往想到业务项目如何集成自动化测试都感受无从下手(还有几回是被比业务代码还多的测试代码吓跑了)。node
可是业务项目里并无太多工具函数须要单元测试(大部分经过 lodash 或其余第三方库来解决复杂逻辑处理),UI 组件也基本是直接采用了业界比较成熟的 ant-mobile, ant-design 等方案,须要在业务项目中开发的 UI 组件并很少,大多数是在第三方的 UI 组件基础上结合业务逻辑进行二次封装(事实上 ant-design 也把本身的组件单独放到 github.com/react-compo… 里维护了)。react
业务项目里须要自动化测试的场景主要是想覆盖用户的主要使用路径,例如登陆注册,加购到购物车,查看操做订单,修改我的信息等等,都是与 UI 界面的渲染逻辑强相关的,须要测试这些页面的表单提交,自动跳转,数据渲染是否有异常。webpack
因此在此能够梳理一下咱们的需求是:git
- 能够模拟用户的点击输入操做,事件驱动来验证页面渲染是否符合预期
- 可使用命令行跑测试,能够集成到 CI
- 轻量高效,环境易搭建,测试代码易编写(毕竟是做为对敏捷开发,持续集成的环节补充,并非 QA 环节的测试,不该舍本逐末)
想到这里不难发现,前端业务项目里最须要的是 E2E 测试,可是在如题图的测试金字塔所示,E2E 测试在金字塔顶端,执行 E2E 测试成本高又速度慢。所以 Cypress 应运而生,Cypress 提供了完备的解决方案,从测试金字塔顶端的 E2E 到集成测试再到单元测试都实现。github
Cypress 是在 Mocha API 的基础上开发的一套开箱即用的 E2E 测试框架,并不依赖前端框架,也无需其余测试工具库,配置简单,而且提供了强大的 GUI 图形工具,能够自动截图录屏,实现时空旅行并在测试流程中 Debug 等等。web
总结一下,Cypress 的优势有:
- 配置简单,可快速集成到现有项目中
- 支持全部等级的测试(即前面所提到的 e2e 测试,集成测试,单元测试等)
- 能够给每一步测试都生成快照,易于 Debug
- 能够获取、操做 Web 页面里的全部 DOM 节点
- 自动重试功能,Cypress 会在当前节点重试几回再判定测试失败
- 易于集成到 CI 系统中
与其它相似测试工具如 Selenium、Puppeteer、Nightwatch 相比,Cypress 的测试代码语法更简单,而且在保证了框架的轻量高效的前提下,对前端工程师更友好。
简单介绍一下使用方法(具体能够参照官网引导):
安装:
yarn add cypress --dev
复制代码
添加到项目的 npm 脚本中:
{
"scripts": {
"cypress:open": "cypress open"
}
}
复制代码
根目录里配置 cypress.json
:
{
"baseUrl": "http://localhost:8080", // 本地启动的 webpack-dev-server 地址
"viewportHeight": 800, // 测试环境的页面视口高度
"viewportWidth": 1280 // 测试环境的页面视口宽度
}
复制代码
npm run cypress:open
复制代码
这就已经在本地打开了测试 GUI,能够进行测试了。
用官方文档的一个例子说明一下测试代码怎么写:
describe('My First Test', function() {
it('Gets, types and asserts', function() {
cy.visit('https://example.cypress.io')
cy.contains('type').click()
// Should be on a new URL which includes '/commands/actions'
cy.url().should('include', '/commands/actions')
// Get an input, type into it and verify that the value has been updated
cy.get('.action-email')
.type('fake@email.com')
.should('have.value', 'fake@email.com')
})
})
复制代码
这其实已经测试了:
测试代码语义化比较好,代码量很少,也不须要写不少 async 逻辑。
到这里为止体验了一下安装配置,本地测试,感受还能够,功能丰富,上手比较简单,集成到项目里也不麻烦。
凡是没有集成到 CI 里的测试都只是玩具,并不能算数。因此咱们来看看 Cypress 这块的表现吧。
咱们但愿 Cypress 能够经过配置,在开发的不一样阶段执行不一样的测试命令。好比在发起 PR 到 feature 分支时能够在当前分支执行集成测试,到 master 主分支时还需计算测试覆盖率并将数据上报到 Sonar 等质检平台(还能够设置测试覆盖率不知足xx%的话则测试失败等等)。
所以咱们先看看测试覆盖率要怎么计算。Cypress 的测试覆盖率计算貌似是后来才添加上的功能,配置稍有点复杂。
依然仍是具体说明能够参照文档,博客中只是简单介绍一下:
首先安装依赖:
npm install -D @cypress/code-coverage nyc istanbul-lib-coverage
复制代码
再配置一下 Cypress 中的配置:
// cypress/support/index.js
import '@cypress/code-coverage/support'
// cypress/plugins/index.js
module.exports = (on, config) => {
on('task', require('@cypress/code-coverage/task'))
}
复制代码
文档只介绍到这里,若是项目用了 TypeScript 的话这就还远远不够,翻了一下官方的 github 示例才发现还须要几个步骤:
npm i -D babel-plugin-istanbul
复制代码
设置一下 .babelrc
{
"plugins": ["istanbul"]
}
复制代码
再修改一下 cypress/plugins/index.js
// cypress/plugins/index.js
module.exports = (on, config) => {
on('task', require('@cypress/code-coverage/task'))
on('file:preprocessor', require('@cypress/code-coverage/use-babelrc'))
}
复制代码
"cy:run": "cypress run && npm run test:report",
"instrument": "nyc instrument --compact=false client instrumented",
"test:report": "npm run instrument && npx nyc report --reporter=text-summary",
复制代码
经过 cypress run
能够直接在命令行跑测试,不启动 GUI,在 CI 里使用的话就该用这个命令。
看看结果,真是快快乐乐。
Cypress 的 E2E 测试的覆盖率也能够和单元测试,或是经过其它框架 Jest 等的测试覆盖率进行合并,具体方法能够去官网查找。
下面咱们来以 Gitlab CI runner 为例来看一下 Cypress 怎么集成到 CI:
// .gitlab-ci.yml
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/Cypress"
stages:
- test
- sonar
cache:
paths:
- .npm
- node_modules/
- cache/Cypress
build:
stage: sonar
tags:
- docker
script:
- yarn
- sh ci/sonar.sh
- yarn build
artifacts:
expire_in: 7 day
paths:
- codeclimate.json
- build
cypress-e2e-local:
image: cypress/base:10
tags:
- docker
stage: test
script:
- unset NODE_OPTIONS
- yarn
- $(npm bin)/cypress cache path
# show all installed versions of Cypress binary
- $(npm bin)/cypress install
- $(npm bin)/cypress cache list
- $(npm bin)/cypress verify
- npm run test
artifacts:
expire_in: 1 week
when: always
paths:
- coverage/lcov.info
- cypress/screenshots
- cypress/videos
复制代码
在这里咱们设置了两个 CI 阶段,test 与 build(与 Sonar 扫描,数据上报等),在 test 阶段中使用了 Cypress 的官方镜像 cypress/base:10
。(其它环境变量设置和依赖如 Sonar 扫描,yarn 等都在咱们本身的 Docker 镜像中)
其中 CI 所执行的命令 npm run test
是:
"test": "start-server-and-test start http://localhost:5000 cy:run"
复制代码
在这里为了简化命令,使用了 npm 包 start-server-and-test 来实现待本地 Server 启动以后再执行测试这一逻辑。
咱们也在 .gitlab-ci.yml
中设置了 artifacts
:
artifacts:
expire_in: 1 week
when: always
paths:
- coverage/lcov.info
- cypress/screenshots
- cypress/videos
复制代码
这是 Gitlab 的 job artifacts 功能,能够设置在某一步骤完成以后将特定文件夹的内容上传到服务器,在有效时间内,咱们能够在网页端查看或下载这些文件内容。这样若是在 CI 测试失败的话咱们就能够在 artifacts 中查看其测试失败视频和快照,避免盲猜式 Debug。
在 CI 设置和测试用例管理中能够深挖的点还有不少。好比将测试用例分为冒烟测试,全量测试,或者 Client 端测试,Node 层测试等等。
Cypress 可应用的测试场景也更多,好比经过设置 Cookie 实现不一样权限用户的测试,引入 Chance.js 实现随机点击 Tab 进行不一样选项卡的测试,Mock 接口返回值等等。
glebbahmutov.com/blog/ 是 Cypress 的主要维护者的博客,其中也记录了不少骚操做(好比检查网页对比度是否知足条件等等),若有兴趣,能够继续进行挖掘。
在个人实践中发现的一个坑点是 Cypress 缺乏对于 fetch
请求的支持(github.com/cypress-io/… mock 请求,只能经过一个有点脏的方法来 hack 解决。
在项目中引入whatwg-fetch,再修改 cypress/support/command.js
:
// cypress/support/command.js
Cypress.Commands.add('visitWithDelWinFetch', (path, opts = {}) => {
cy.visit(
path,
Object.assign(opts, {
onBeforeLoad(win) {
delete win.fetch;
},
})
);
});
复制代码
这样咱们就能够测试咱们项目的登陆重定向判断了:
describe('Node server', function() {
it('no cookie get 401', function() {
cy.server()
cy.clearCookies()
cy.route('POST', '**/graphql').as('login')
cy.visitWithDelWinFetch('/');
cy.wait('@login').then((xhr) => {
expect(xhr.status).to.eq(401)
})
})
it('with cookie get 200', function() {
cy.server()
cy.route('POST', '**/graphql').as('loginWithCookie')
cy.visitWithCookie('/');
cy.wait('@loginWithCookie').then((xhr) => {
expect(xhr.status).to.eq(200)
})
// login successfully, so display the content
cy.get('.ant-layout-sider')
cy.get('.ant-layout-content')
})
})
复制代码
aha~