Cypress 簡介
- 基於 JavaScript 的前端測試工具,可以對瀏覽器中運行的任何內容進行快速、簡單、可靠的測試
- Cypress 是自集成的,提供了一套完整的端到端測試,無須藉助其他外部工具,安裝後即可快速地創建、編寫、運行測試用例,且對每一步操作都支持回看
- 不同於其他只能測試 UI 層的前端測試工具,Cypress 允許編寫所有類型的測試,覆蓋了測試金字塔模型的所有測試類型【界面測試,集成測試,單元測試】
- Cypress 底層協議不採用 WebDriver
Cypress 原理
Webdriver 運行的方式
- 大多數測試工具(如:Selenium/webdriver)通過在外部瀏覽器運行並在網絡上執行遠程命令來運行
- 因為 Webdriver 底層通信協議基於 JSON Wire Protocol,運行需要網絡通信
Cypress 運行的方式
Cypress 和 Webdriver 方式完全相反,它與應用程序在相同的生命週期裏執行
Cypress 運行測試的大致流程
運行測試後,Cypress 使用 webpack 將測試代碼中的所有模塊 bundle 到一個 js 文件中
然後,運行瀏覽器,並且將測試代碼注入到一個空白頁中,然後它將在瀏覽器中運行測試代碼【可以理解成:Cypress 將測試代碼放到一個 iframe 中運行】
Cypress 運行測試的技術流程
- 每次測試首次加載 Cypress 時,內部 Cypress Web 應用程序先把自己託管在本地的一個隨機端口上【如:http://localhost:65874】
- 在識別出測試中發出的第一個 cy.visit() 命令後,Cypress 會更改本地 URL 以匹配你遠程應用程序的 Origin【滿足同源策略】,這使得你的測試代碼和應用程序可以在同一個 Run Loop 中運行
Cypress 運行更快的根本原因
- Cypress 測試代碼和應用程序均運行在由 Cypress 全權控制的瀏覽器中
- 且它們運行在同一個Domain 下的不同 iframe 中,所以 Cypress 的測試代碼可以直接操作 DOM、Window Objects、Local Storages而無須通過網絡訪問
Cypress 穩定性、可靠性更高的原因
- Cypress 還可以在網絡層進行即時讀取和更改網絡流量的操作
- Cypress 背後是 Node.js Process 控制的 Proxy 進行轉發,這使得 Cypress 不僅可以修改進出瀏覽器的所有內容,還可以更改可能影響自動化操作的代碼
- Cypress 相對於其他測試工具來説,能從根本上控制整個自動化測試的流程
Cypress 架構圖
Cypress 的特性
時間穿梭【歷史記錄】
- Cypress 在測試代碼運行時會自動拍照
- 等測試運行結束後,用户可在 Cypress 提供的 Test Runner 裏,通過懸停在命令上的方式查看運行時每一步都發生了什麼
實時重新加載
當測試代碼修改保存後,Cypress 會自動加載改動地方,並重新運行測試
Spies(間諜)、Stubs(存根)、Clock(時鐘)
- Cypress 允許你驗證並控制函數行為,Mock 服務器的響應,更改系統時間
- 單元測試觸手可及!
運行結果一致性
Cypress 架構不使用 Selenium 或 Webdriver,在運行速度、可靠性測試、測試結果一致性上均有良好保障
可調試性
當測試失敗時,可以直接從開發者工具(F12 Chrome DevTools)進行調試,這熟悉吧??
自動等待
- 使用Cypress,永遠無須在測試中添加 強制等待、隱性等待、顯性等待
- Cypress 會自動等待元素至可靠操作狀態時才執行命令或斷言
- 異步操作觸手可及!
網絡流量控制
Cypress 可以 Mock 服務器返回的結果,無須依賴後端服務器,即可實現模擬網絡請求
截圖和視頻
Cypress 在測試運行失敗時會自動截圖,在無頭運行時(無GUI界面)會錄製整個測試套件的視頻
Cypress 優勢的總結
歸納起來,具體優點如下:
- 自集成大部分測試所需的庫
像我們在用 Selenium 時,需要集成單元測試框架(unittest、pytest),想要好看的測試報告還得集成(allure),想要 Mock 還得引入對應的 Mock 庫而 Cypress 是開箱即用!啥意思?看下圖!
- 執行速度快,方便調試。
- 適用於中小項目,統一技術棧的團隊。
- 自集成測試截圖,很方便。
Cypress 缺點
- 學習成本過於高,cypress的框架註定只能使用js,且對測試來説js的性價比沒有python高,selenium還支持各種語言。
- 坑坑窪窪太多,cypress在最新的教程都在2020年8月才出,一些莫名的bug和潛在的知識點都無從知曉。
- cypress使用的是異步調用,不太懂的在這個坑裏爬都爬不出。
- cypress最佳的使用者其實是前端,但一般前端不幹這個。
- 靈活性沒selenium高,selenium有太多的庫支持其完成騷操作。
- 對數據的處理沒有python好用,js在一些數據處理上會有莫名的問題,不太懂前端就有點尷尬。
默認文件結構
在使用 cypress open 命令首次打開 Cypress,Cypress 會自動進行初始化配置並生成一個默認的文件夾結構,如下圖
fixtures 測試夾具
簡介
- 測試夾具通常配合 cy.fixture() 使用
- 主要用來存儲測試用例的外部靜態數據
- fixtures 默認就在 cypress/fixtures 目錄下,但也可以配置到另一個目錄
外部靜態數據的詳解
- 測試夾具的靜態數據通常存儲在 .json 文件中,如自動生成的 examples.json
- 靜態數據通常是某個網絡請求對應的響應部分,包括HTTP狀態碼和返回值,一般是複製過來更改而不是自己手工填寫
fixtures 的實際應用場景
如果你的測試需要對某些外部接口進行訪問並依賴它的返回值,則可以使用測試夾具而無須真正訪問這個接口(有點類似 mock)
使用測試夾具的好處
- 消除了對外部功能模塊的依賴
- 已編寫的測試用例可以使用測試夾具提供的固定返回值,並且你確切知道這個返回值是你想要的
- 因為無須真正地發送網絡請求,所以測試更快
命令示例
要查看 Cypress 中每個命令的示例,可以打開 cypress/integration/examples ,裏面都是官方提供的栗子
test file 測試文件
簡介
測試文件就是測試用例,默認位於 cypress/integration ,但也可以配置到另一個目錄
測試文件格式
- 所有在 integration 文件下,且文件格式是以下的文件都將被 Cypress 識別為測試文件
- .js :普通的JavaScript 編寫的文件【最常用啦】
- .jsx :帶有擴展的 JavaScript 文件,其中可以包含處理 XML 的 ECMAScript
- .coffee :一套 JavaScript 轉譯的語言。有更嚴格的語法
- .cjsx :CoffeeScript 中的 jsx 文件
創建好後,Cypress 的 Test Runner 刷新之後就可以看到對應測試文件了
plugin file 插件文件
前言
- Cypress 獨有優點就是測試代碼運行在瀏覽器之內,使得 Cypress 跟其他的測試框架相比,有顯著的架構優勢
- 這優點雖然提供了可靠性測試,但也使得和在瀏覽器之外進行通信更加困難【痛點:和外部通信困難】
插件文件的誕生
- Cypress 為了解決上述痛點提供了一些現成的插件,使你可以修改或擴展 Cypress 的內部行為(如:動態修改配置信息和環境變量等),也可以自定義自己的插件
- 默認情況,插件位於 cypress/plugins/index.js 中,但可以配置到另一個目錄
- 為了方便,每個測試文件運行之前,Cypress 都會自動加載插件文件 cypress/plugins/index.js
插件的應用場景
- 動態更改來自 cypress.json,cypress.env.json,CLI或系統環境變量的已解析配置和環境變量
- 修改特定瀏覽器的啓動參數
- 將消息直接從測試代碼傳遞到後端
support file 支持文件
簡介
- 支持文件目錄是放置可重用配置項,如底層通用函數或全局默認配置
- 支持文件默認位於 cypress/support/index.js 中,但可以配置到另一個目錄
- 為了方便,每個測試文件運行之前,Cypress 都會自動加載支持文件 cypress/support/index.js
如何使用支持文件加載錢包
只需要在 cypress/support/commands.ts 文件裏配置即可,原生的文件是commands.js,現在將其後綴改為ts。
還配置了一個commands.d.ts。相關代碼如下
commands.js中,需要使用三個庫:JsonRpcProvider、Wallet、Eip1193Bridge,還需要在index.js中導入commands,如下:
Commands的命令有3個,
Cypress.Commands.add(name, callbackFn)、
Cypress.Commands.add(name, options, callbackFn) 、
Cypress.Commands.overwrite(name, callbackFn)
參數説明
- name:要添加或覆蓋的命令的名稱
- callbackFn :自定義命令的回調函數,回調函數裏自定義函數所需完成的操作步驟
- options:允許自定義命令的隱性行為
如下,在visit函數中注入錢包,因為每次執行用例都會使用visit進入網址。測試用例如下:
commands.ts文件其他代碼
// ***********************************************
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
import { JsonRpcProvider } from '@ethersproject/providers'
import { Wallet } from '@ethersproject/wallet'
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
const TEST_PRIVATE_KEY = Cypress.env('INTEGRATION_TEST_PRIVATE_KEY')
// address of the above key
export const TEST_ADDRESS_NEVER_USE = new Wallet(TEST_PRIVATE_KEY).address
export const TEST_ADDRESS_NEVER_USE_SHORTENED = `${TEST_ADDRESS_NEVER_USE.substr(
0,
6
)}...${TEST_ADDRESS_NEVER_USE.substr(-4, 4)}`
class CustomizedBridge extends Eip1193Bridge {
chainId = 256
async sendAsync(...args) {
console.debug('sendAsync called', ...args)
return this.send(...args)
}
async send(...args) {
console.debug('send called', ...args)
const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function'
let callback
let method
let params
if (isCallbackForm) {
callback = args[1]
method = args[0].method
params = args[0].params
} else {
method = args[0]
params = args[1]
}
if (method === 'eth_requestAccounts' || method === 'eth_accounts') {
if (isCallbackForm) {
callback({ result: [TEST_ADDRESS_NEVER_USE] })
} else {
return Promise.resolve([TEST_ADDRESS_NEVER_USE])
}
}
if (method === 'eth_chainId') {
if (isCallbackForm) {
callback(null, { result: '0x100' })
} else {
return Promise.resolve('0x100')
}
}
try {
const result = await super.send(method, params)
console.debug('result received', method, params, result)
if (isCallbackForm) {
callback(null, { result })
} else {
return result
}
} catch (error) {
if (isCallbackForm) {
callback(error, null)
} else {
throw error
}
}
}
}
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
Cypress.Commands.overwrite('visit', (original, url, options) => {
return original(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `${url}` : url, {
...options,
onBeforeLoad(win) {
options && options.onBeforeLoad && options.onBeforeLoad(win)
win.localStorage.clear()
const provider = new JsonRpcProvider('https://http-testnet.hecochain.com', 256)
const signer = new Wallet(TEST_PRIVATE_KEY, provider)
win.ethereum = new CustomizedBridge(signer, provider)
},
})
})
參考資料
Cypress官方文檔
歡迎區塊鏈行業志同道合的小夥伴添加小極微信,加入blockgeek區塊鏈技術交流羣,共同推動區塊鏈技術普及和發展~