關於前端測試的一些理論與基於 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~