【第八期】使用 Cypress 輔助前端 TDD 研發入門

咱們來看一則小故事:javascript

一個初創公司,在早期,全部的業務界面都在一個前端工程項目中,工程裏有五個界面,一個研發加上一個測試,就能保證每次發佈的新功能穩定可靠。html

隨着公司業務蓬勃發展,當年的初創公司慢慢變得愈來愈壯大,業務界面變得愈來愈多,原來的那個前端工程項目從只有五個界面,增加到如今的幾十個界面。前端

而且,業務線也變多了。經過對業務線的劃分,甚至同一個業務線內,不一樣類型的模塊劃分,公司的業務界面總量達到上千個,分別存放在幾十個前端工程項目中。java

相對的,前端團隊的研發人員和測試人員的數量也由原來的兩我的,增加到如今的幾十人。git

這時,團隊遭遇了一個問題:每次發佈上線的功能,變得再也不穩定可靠,常常出現各類各樣的問題github

這個狀態一直在持續,而公司也不得不持續不斷得爲這些問題形成的影響買單。web

公司開始着手調查這個問題的本質緣由。公司派出了兩位調查員:小A 和 小B。chrome

小A認爲人才資源仍是不足,須要招聘更多的人,讓每一個前端業務項目都能有足夠的人來維護,借鑑早期的團隊結構,也就是達到每一個工程五個界面,一個研發和一個測試的人員規模。數據庫

小B認爲現有前端團隊的工做方式須要調整,以適應大規模的前端工程維護與建設。並給出了兩個實際案例:npm

案例一:某個前端工程內有一百多個界面,最近用戶反饋某個界面打開後沒有內容。反饋信息到達研發團隊,研發人員調查發如今幾個月以前這個界面被其它界面的修改影響了,致使此問題。當時測試人員並無發現這個問題,由於一方面測試人員並無這個模塊全部界面的名單;另外一方面當時修改的是另外的界面,測試人員將精力放在那些被修改的界面上了。最後,研發人員修復了這個界面,並交給測試人員進行測試,測試人員測試經過後發佈上線,修復了這個問題。

案例二:某個前端工程最近沒有發佈動做,但某天忽然收到用戶反饋:建立訂單時,提示訂單建立失敗。反饋信息到達研發團隊,研發人員調查發現最近安全策略模塊上線更新了某條安全策略,新的安全策略規則過於寬泛,阻止了這位反饋用戶的訂單內容。一方面研發人員引導用戶修復了非法的訂單內容;另外一方面,研發人員調整了致使問題的安全策略規則,最終解決了這個問題。

在參考了兩位調查員的報告後,團隊作出了以下調整:

在測試環境增長自動測試,自動測試分爲兩個類型:冒煙測試(探測界面是否有內容) 和 功能測試(探測功能是否暢通)

自動測試的時間設定爲:上午10點 和 晚上6點

測試的結果以交通燈的方式展現,並掛鉤到發佈平臺:紅色燈表明測試失敗、黃色燈表明測試未完成、綠色燈表明測試經過

發佈平臺只容許綠色燈的工程項目發佈上線

這則小故事結束了,咱們來回顧這則故事中出現的兩個問題:

  • 隨着工程模塊內的複雜度不斷增長,靠人的精力來覆蓋工程內的方方面面,已不切實際。
  • 隨着不一樣工程模塊的規模不斷壯大,靠人的精力來控制不一樣工程之間的關係和影響,已不切實際。

專業的工程師都明白一個道理:人是很容易犯錯的,一個功能,靠人自覺或當心謹慎地來維持,無異於做繭自縛。

測試 是一個很大的話題,咱們今天來聊一下 測試驅動開發

提及 測試驅動開發 (Test Driven Development, TDD),相信不少讀者都並不陌生。

測試驅動開發(英語:Test-driven development,縮寫爲TDD)是一種軟件開發過程當中的應用方法,由極限編程中倡導,以其倡導先寫測試程序,而後編碼實現其功能得名。測試驅動開發始於20世紀90年代。測試驅動開發的目的是取得快速反饋並使用「illustrate the main line」方法來構建程序。

測試驅動開發是戴兩頂帽子思考的開發方式:先戴上實現功能的帽子,在測試的輔助下,快速實現其功能;再戴上重構的帽子,在測試的保護下,經過去除冗餘的代碼,提升代碼質量。測試驅動着整個開發過程:首先,驅動代碼的設計和功能的實現;其後,驅動代碼的再設計和重構。

--- 維基百科

要說在實際工做中使用 TDD 的方式進行開發,可能只有少數人會這麼作。

由於,考慮到有限的、甚至急迫的業務研發週期,相信不少讀者都會感到擔心:測試會佔用開發週期,不如直接開發功能更 「省」 時間。

若是咱們用更長遠的視角來看研發這件事情,就會發現,實際上對於直接開發功能的方式,只是將軟件工程的時間提早消費了,後期仍是得將前期 「省」 去的那部分時間還回來,甚至還要支付昂貴的利息(固然了,重視短時間利益的人並不會想那麼遠)。

測試的重要性不言而喻,咱們發現,將測試放在軟件工程的越靠前的環節中,它就越能幫助到工程自己,由於:

  1. 測試能在代碼投入生產以前,發現其中潛在的問題,避免損失
  2. 測試能促使代碼變得更加健壯,提升可維護性,從而下降成本
  3. 測試能發現設計的不良之處,促進改良設計,從而提升服務質量

今天,咱們來學習一個叫作 Cypress 的測試工具,使用這個工具,來幫助咱們進行前端功能研發。

咱們按照下面的步驟逐步講解:

  1. 什麼是 Cypress
  2. 如何安裝 Cypress
  3. 開發前:如何寫基於 Cypress 的測試文件
  4. 開發中:如何使用 Cypress 測試文件輔助咱們進行測試
  5. 如何更方便得寫 Cypress 測試文件

1. 什麼是 Cypress

cypress 是一個完整的,易用的測試框架

咱們可使用 Cypress 進行: e2e測試、集成測試、單元測試

Cypress 官網介紹瞭如下功能特色:

  • 時間旅行:Cypress 會在測試運行是拍攝快照,只需將鼠標懸停在命令日誌中的命令上,便可確切瞭解每一個步驟發生的狀況。
  • 可調式:咱們無需猜想測試用例爲什麼失敗,直接從熟悉的工具進行調試(例如:谷歌瀏覽器的開發者工具),可讀錯誤和堆棧跟蹤讓調試更有效率。
  • 自動等待:再也無需在測試用例代碼中添加 waitsleep 代碼,Cypress 會自動等待命令和斷言完成。
  • 函數間諜、響應劫持、時鐘回撥:驗證和控制函數、服務器響應和時鐘。經常使用的單元測試功能,cypress 都已提供於指尖。
  • 網絡通訊控制:無需涉及服務器便可控制、保存和測試邊緣狀況。你能夠根據須要保留網絡流量。
  • 一致的結果:Cypress 的架構不使用 SeleniumWebDriver。讓測試更快速,一致和可靠。
  • 視圖快照和視頻:從命令行運行測試時,咱們能夠查看失敗用例的視圖快照和整個測試過程的視頻。

除了以上這些功能外,Cypress 還有以下不足之處:

  • 不擅長瀏覽器兼容性測試
  • 不擅長微信、微博等 Oauth2.0 受權登陸測試
  • 只能測試 web 頁面(不能測試小程序)
  • 尺寸較大,下載安裝時間較長

2. 如何安裝 Cypress

2.1 下載

由於 Cypress 的體積相對較大(接近 150mb),因此咱們接下來經過瀏覽器下載安裝 Cypress 的過程。

您也能夠經過 npmyarn 來安裝 Cypress

咱們打開 Cypress 官網,點擊官網首頁的 Download Now 連接開始下載 Cypress

下載完成後,咱們獲得一個名爲 cypress.zip 的壓縮包文件。

解壓縮後,咱們會獲得一個名爲 Cypress 的可執行文件。

2.2 建立工做區

咱們在 home 目錄下建立一個名爲 cypress_demo 的空文件夾(用於存放 cypress 測試文件),做爲 Cypress 的工做區。

mkdir $HOME/cypress_demo
複製代碼

咱們打開 Cypress 執行文件,會看到以下界面:

點擊 select manually,手動找到並選擇咱們剛纔建立的工做區文件夾 $HOME/cypress_demo,而後點擊 打開

這時,咱們會看到 Cypress 自動爲咱們在 cypress_demo 文件夾中生成了一個叫作 cypress 的文件夾和一個叫作 cypress.json 的配置文件。

cypress 文件夾中,有 4 個子文件夾,分別是:

  • fixtures 存放一些測試用例中須要用到的靜態資源,好比:數據庫模擬數據(json格式)、圖片等信息
  • integration 存放 Cypress 測試文件
  • plugins 存放 Cypress 插件
  • support 存放 Cypress 自定義命令

其中,在 integration 文件夾中,Cypress 還爲咱們生成了一些測試樣例文件,方便咱們參考和學習。

咱們在 Cypress 的運行界面中能夠看到這些樣例文件:

完整目錄結構以下:

cypress_demo
├── cypress # Cypress 工做目錄
│   ├── fixtures # 存放一些測試用例中須要用到的資源,好比:數據庫模擬數據、圖片、json信息等等
│   │   └── example.json
│   ├── integration # 存放 Cypress 測試文件
│   │   └── examples # 這個文件夾中存放了 Cypress 官方提供的一些測試樣例
│   │       ├── actions.spec.js
│   │       ├── aliasing.spec.js
│   │       ├── assertions.spec.js
│   │       ├── connectors.spec.js
│   │       ├── cookies.spec.js
│   │       ├── cypress_api.spec.js
│   │       ├── files.spec.js
│   │       ├── local_storage.spec.js
│   │       ├── location.spec.js
│   │       ├── misc.spec.js
│   │       ├── navigation.spec.js
│   │       ├── network_requests.spec.js
│   │       ├── querying.spec.js
│   │       ├── spies_stubs_clocks.spec.js
│   │       ├── traversal.spec.js
│   │       ├── utilities.spec.js
│   │       ├── viewport.spec.js
│   │       ├── waiting.spec.js
│   │       └── window.spec.js
│   ├── plugins # 存放 Cypress 插件
│   │   └── index.js
│   └── support # 存放 Cypress 自定義命令
│       ├── commands.js
│       └── index.js
└── cypress.json # Cypress 配置文件
複製代碼

2.3 配置 Cypress

接下來,咱們來配置 Cypress,根據上一小節的內容,咱們知道,配置 Cypress,須要經過 cypress.json 這個文件。

那麼咱們具體能在裏面作哪些配置呢?完整的配置內容,請參考官網 配置指南

這裏,咱們先關注下面兩個配置項:

  1. chromeWebSecurity
  2. userAgent

2.3.1 chromeWebSecurity

chromeWebSecurity 決定是否開啓 chrome 瀏覽器針對同源策略和不安全的混合內容的安全策略。

它默認是開啓狀態

實際上,這給咱們的測試文件內容帶來了一些限制,完整的限制名單請參考官網 Web安全限制

這裏舉個例子,咱們在同一個測試用例中分別訪問一個主域下的資源,Cypress 容許咱們這麼作:

cy.visit('https://www.cypress.io')
cy.visit('https://docs.cypress.io') // yup all good
複製代碼

但是,一旦咱們在同一個測試用例中,分別訪問不一樣主域下的資源,Cypress 默認會阻止咱們:

cy.visit('https://apple.com')
cy.visit('https://google.com')      // this will immediately error
複製代碼

爲了接近實際的測試場景,咱們在 cypress.json 中將它關閉,以方便咱們接下來的測試工做:

{
  "chromeWebSecurity": false
}
複製代碼

注意即便咱們將 chromeWebSecurity 關閉,Cypress 也依然不容許在同一個測試用例中使用 cy.visit 訪問兩個不一樣的頂級域名,詳情請參考 #944

2.3.2 userAgent

userAgent 決定 Cypress 訪問任何網絡資源時,來自哪一個操做系統、哪一個瀏覽器、瀏覽器版本。

這裏假設咱們平時使用微信開發者工具來開發微信內 h5 頁面應用,因此咱們將 userAgent 的值設置爲微信開發者工具:

{
  "chromeWebSecurity": false,
  "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 wechatdevtools/1.02.1904090 MicroMessenger/6.5.7 Language/zh_CN webview/15602362378809380 webdebugger port/39554"
}
複製代碼

至此,Cypress 的環境配置就完成了。

3. 開發前:如何寫基於 Cypress 的測試文件

每一個測試文件,都遵循一個模式,大體以下:

  • 表明一個測試組的名稱和做用域,在 Cypress 中,它叫 describe
  • 表明整個測試組執行前,進行初始化工做的做用域,在 Cypress 中,它叫 before
  • 表明整個測試組執行後,進行收尾清理工做的做用域,在 Cypress 中,它叫 after
  • 表明每一個測試用例執行前,進行處理工做的做用域,在 Cypress 中,它叫 beforeEach
  • 表明每一個測試用例執行後,進行處理工做的做用域,在 Cypress 中,它叫 afterEach
  • 表明一個測試用例的名稱和做用域,在 Cypress 中,它叫 it

以上這些抽象概念,在 Cypress 中以以下形式展現:

describe('測試組名稱', () => {
 
  before(() => {
    console.log(' --- 在當前 describe 中全部 it 執行前,運行一次 --- ')
  })
 
  after(() => {
    console.log(' --- 在當前 describe 中全部 it 執行後,運行一次 --- ')
  })
 
  beforeEach(() => {
    console.log(' --- 在當前 describe 中每一個 it 執行前,運行一次 --- ')
  })
 
  afterEach(() => {
    console.log(' --- 在當前 describe 中每一個 it 執行後,運行一次 --- ')
  })
 
  it('測試用例1', () => {
    // 在這裏寫測試邏輯...
  })
 
  it('測試用例2', () => {
    // 在這裏寫測試邏輯...
  })
 
})
複製代碼

在開始學習寫測試用例的內容以前,咱們先思考一下:

開發一個簡單的登錄頁面,咱們平時都是如何測試這個頁面的呢?

假設:

  • 登陸頁面的地址爲: http://www.demo.com/login
  • 登陸成功後,會跳轉到地址:http://www.demo.com/main
  • 用戶名爲:demo
  • 密碼爲: password
  • 用戶名的表單 id 爲: user
  • 密碼的表單 id 爲: pwd
  • 提交按鈕的 id 爲: submit

咱們平時會手動執行的測試步驟:

  • 第一步:在瀏覽器地址欄輸入登錄頁面的地址
  • 第二步:在登錄頁面中,填寫用戶名和密碼,而後點擊登陸
  • 第三步:檢查登陸後的狀態,看是否登陸成功

將以上步驟轉換爲 Cypress 能理解的語言,以下:

describe('登陸測試', function () {
  it('成功的 case', function() {
    // 打開登陸頁面
    cy.visit('http://www.demo.com/login')
    // 輸入用戶名
    cy.get('#user').type('demo')
    // 輸入密碼
    cy.get('#pwd').type('password')
    // 點擊提交按鈕
    cy.get('#submit').click()
    // 判斷是否登陸成功
    cy.url().should('include', '/main')
  })
})
複製代碼

Cypress 中:

  • 訪問一個網絡資源,使用 cy.visit
  • 獲取網頁上某個元素,使用 cy.get
  • 向某個表單元素輸入內容,使用 cy.type
  • 在某個元素上觸發點擊事件,使用 cy.click
  • 獲取當前 url 地址,使用 cy.url
  • 建立一個斷言,使用 cy.should

4. 開發中:如何使用 Cypress 測試文件輔助咱們進行測試

咱們在文件夾 cypress_demo/cypress/integration/ 下新建一個名爲 demo.js 的文件,將上一小節的代碼拷貝進去,而後保存。

接着,讓咱們回到 2.2 節中打開的 Cypress 執行文件界面(若是您已經關閉了這個界面,只須要從新打開 Cypress 執行文件便可)。

能夠看到咱們剛纔新建的 demo.js 文件了:

點擊它,會開始運行測試文件

由於咱們尚未開始寫這個登陸頁面,因此運行會出錯。

注意

請在本地 host 文件中將 www.demo.com 指向您的開發機器 ip,或使用您本身的域名;

咱們在本篇文章中使用本地 ip: 127.0.0.1www.demo.com 域名

接下來,讓咱們完成登陸頁的開發工做,這些工做不在本篇文章的範圍內。

在咱們完成登陸頁開發工做後,點擊 Cypress 界面的刷新按鈕,再次運行測試文件,能夠看到,測試已經經過了:

5. 如何更方便得寫 Cypress 測試文件

在第 4 節中的例子,其實是很是簡單的

咱們實際開發中,可能須要直接測試某個受限資源(須要登陸後才能訪問),假設咱們的登陸信息都存儲在 cookie 中。

對於表單,除了簡單的文字輸入,咱們也可能須要上傳圖片(好比:上傳頭像),假設咱們要上傳的圖片名稱爲 f.png,咱們須要提早將其拷貝到文件夾 cypress_demo/cypress/fixtures/ 中。

甚至咱們可能須要爲某個前端工程內全部的頁面寫冒煙測試(smoke testing)。

Cypress 中,以上這些通用的功能,均可以經過自定義命令來方便得使用:

cypress_demo
├── cypress
│   └── support
│       ├── commands.js # 在這個文件裏手動添加下面的命令內容
複製代碼

咱們在 commands.js 中添加咱們須要的命令:

// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
 
Cypress.Commands.add('uploadImage', (fileName, fileType = ' ', selector) => {
  cy.get(selector).then(subject => {
    cy.fixture(fileName, 'base64')
      .then(Cypress.Blob.base64StringToBlob)
      .then(blob => {
        const el = subject[0]
        const testFile = new File([blob], fileName, { type: fileType })
        const dataTransfer = new DataTransfer()
        dataTransfer.items.add(testFile)
        el.files = dataTransfer.files
        subject.trigger('change')
      })
  })
})
 
Cypress.Commands.add('login', (metaData) => {
  Object.keys(metaData).forEach(key => {
    cy.setCookie(key, JSON.stringify(metaData[key]))
  })
})

Cypress.Commands.add('smoke', selector => {
  cy.get(selector).then($app => {
    let text = String.prototype.trim.call($app.text())
    if (text.length) {
      // 冒煙測試的標準爲: 頁面是否有文本內容
      expect(text.length).to.be.gt(0)
    } else {
      // 或者是否有圖片
      expect($app.find('img').length).to.be.gt(0)
    }
  })
})
複製代碼

而後,咱們就能夠在測試文件中使用這些命令了:

describe('測試組標題', () => {
 
  before(() => {
    cy.login({token:'您的token', refreshToken: '您的refreshToken'})
  })
 
  it('測試登陸後才能訪問的資源', () => {
    // 在這裏寫您的測試邏輯
  })

  it('測試上傳圖片', () => {
    cy.visit('https://www.yourdomain.com/page2')

    const fileName = 'f.png'
    const fileType = 'image/png'
    const uploadFileSelector = 'input[type=file]'

    cy.uploadImage(fileName, fileType, uploadFileSelector)
    
    ...

  })
 
  it('冒煙測試', () => {
    let pages = [
      'https://www.yourdomain.com/page1',
      'https://www.yourdomain.com/page2',
      'https://www.yourdomain.com/page2'
    ]

    pages.forEach(page => {
      cy.visit(page)
      cy.wait(1000)
      cy.smoke('body div[id]')
    })
  })
 
})
複製代碼

除了自定義命令外,Cypress 還支持請求攔截、截圖快照和視頻,另外,咱們還能夠經過插件來擴展 Cypress

以上這些功能,本篇文章再也不展開,感興趣的讀者能夠閱讀下面列出的文章。

關於請求攔截,請參考官網 請求攔截器

關於截圖快照和視頻,請參考官網 截圖快照和視頻

關於插件,請參考官網 如何寫插件

最後,讓咱們思考下面幾個問題:

  • Cypress 的測試文件,能夠反覆使用麼?
  • 能夠定時運行 Cypress 的測試文件麼?
  • 咱們如何利用 Cypress 讓咱們的工做變得更加輕鬆?

感謝您花時間閱讀這篇文章,但願這篇文章能對您有所幫助。


水滴前端團隊招募夥伴,歡迎投遞簡歷到郵箱:fed@shuidihuzhu.com

相關文章
相關標籤/搜索