博客 / 詳情

返回

Cypress 框架的介紹

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 運行測試的技術流程

  1. 每次測試首次加載 Cypress 時,內部 Cypress Web 應用程序先把自己託管在本地的一個隨機端口上【如:http://localhost:65874】
  2. 在識別出測試中發出的第一個 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 優勢的總結

歸納起來,具體優點如下:

  1. 自集成大部分測試所需的庫
    像我們在用 Selenium 時,需要集成單元測試框架(unittest、pytest),想要好看的測試報告還得集成(allure),想要 Mock 還得引入對應的 Mock 庫而 Cypress 是開箱即用!啥意思?看下圖!

  1. 執行速度快,方便調試。
  2. 適用於中小項目,統一技術棧的團隊。
  3. 自集成測試截圖,很方便。

Cypress 缺點

  1. 學習成本過於高,cypress的框架註定只能使用js,且對測試來説js的性價比沒有python高,selenium還支持各種語言。
  2. 坑坑窪窪太多,cypress在最新的教程都在2020年8月才出,一些莫名的bug和潛在的知識點都無從知曉。
  3. cypress使用的是異步調用,不太懂的在這個坑裏爬都爬不出。
  4. cypress最佳的使用者其實是前端,但一般前端不幹這個。
  5. 靈活性沒selenium高,selenium有太多的庫支持其完成騷操作。
  6. 對數據的處理沒有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區塊鏈技術交流羣,共同推動區塊鏈技術普及和發展~

image.png

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.