E2E測試框架Cypress教學與其他框架比較

Hao
25 min readSep 16, 2021

--

概述

教學 E2E 測試(端對端測試, End-to-end testing)框架 Cypress 入門以及一些進階技巧、測試情境分享

並且與現在較流行的 E2E 測試框架做比較,對象有 Protractor、Selenium、Nightwatch

官方網站

Cypress GitHub

Protractor

Selenium

Nightwatch

結論

我覺得 Cypress 最好用、最有助於開發。因此本篇文章會多著墨於 Cypress,其他則是簡單帶過

如果對其他框架沒興趣,這篇文章可以只看 Cypress

比較

GitHub 星數統計至 2021/9/16

什麼是 WebDriver?

舊稱 Selenium WebDriver,現為 W3C 管理,稱為 WebDriver API

這是用來操作瀏覽器的一個 API 介面,實作則根據不同瀏覽器有不同的 Driver

其中 Protractor、Selenium、NightWatch 都是基於 WebDriver API 實作的

WebDriver 有什麼問題?

對彈出式視窗 (Pop-up Windows) 不友善

彈出式視窗分為以下三種

  1. Simple — 純粹的提示,只有一個 “確認” 按鈕,主要是讓用戶知道某些訊息
  2. Prompt — 會要求用戶輸入一些資訊,例如 E-mail
  3. Confirmation — 通常有 2 個按鈕,要求用戶同意/拒絕
// 切換到 Alert 視窗
myAlert = browser.switchTo().alert();
// 按下 'OK' 按鈕
myAlert.accept();
// 輸入資訊
myAlert.sendKeys("My name is Hao");

捕捉動態內容 (Dynamic Content) 不方便

動態內容是指一開始不存在網頁上的內容,可能是要按下某個按鈕才會載入的內容

WebDriver 的解決方法是 Explicit Wait,這是強制 WebDriver 等待直到某個條件出現為止才繼續往下做,例如以下 C# 程式碼

// 等待直到 CSS 屬性為 .btn 的按鈕出現
wait.Until(ExpectedConditions.ElementToBeClickable(By.CssSelector(".btn")));

而 Cypress 我用起來對於 DOM 物件的搜尋都會自動等待,不需要做這些額外處理

速度較慢

WebDriver 是跨處理序 (Out-Of-Process) 的,而 Cypress 是直接呼叫 DOM event,相較下 Cypress 的執行速度會比較快

可參考

Difference between in-process and out-process service

DOM event

Cypress

分析

  1. 非常詳細的文件說明,且每個說明頁都附上影片
  2. 內建許多測試範例,範例同時具有每一步快照、程式碼。把範例看完就幾乎等於會使用 Cypress
  3. 優美易用的 UI
  4. 每一步測試都有快照,有助於開發與 Debug

  1. 目前只支援基於 Chromium 的瀏覽器,目前僅限 Chrome、Edge、Electron

說明

安裝

NPM

npm install cypress --save-dev

Yarn

yarn add cypress --dev

開啟 Cypress

npx cypress open

會看到一個介面,內建許多測試範例,點擊即可執行測試。其餘管理都可以透過介面操作

下方圖片是打開測試範例的測試介面,會另開一個瀏覽器執行給你看。左方是測試的項目,右方是執行測試的瀏覽器。範例使用的都是他們自己的網站,除了要測試的 UI 元件之外也都直接在網頁上附上程式碼

我強烈建議將範例全部看一次,搭配這個測試的程式碼以及每一步的快照,看範例就像在讀文件一樣,非常清楚,看完等於學完整套 Cypress 的基礎。所以這邊不贅述 Cypress 的基本語法怎麼寫

將滑鼠移動到左方的測試步驟上,右方的測試介面就會顯示 before、after,顯示這一步的執行前後差異

範例

// Google.jsit('搜尋', () => {
// 前往Google搜尋網頁
cy.visit('https://www.google.com/?hl=zh_tw')
// 找到輸入框輸入 'YouTube'
// 同時驗證輸入框是否真的有 'YouTube' 這個值
cy.get('[name=q]')
.type('YouTube').should('have.value', 'YouTube')
// 找到搜尋按鈕按下
// 使用.first()的原因在於name=btnK的元素有2個
cy.get('[name=btnK]').first().click()

// 取得網頁標題 同時驗證應該等於 'YouTube - Google 搜尋'
cy.title().should('eq', 'YouTube - Google 搜尋')
})

執行

透過 UI 操作即可,若要跑在 Console,則使用以下指令

# 執行指定測試
npx cypress run --spec "project/testFile.js"
# 執行目錄底下所有測試
npx cypress run --spec "project/**"

由於預設瀏覽器是 Electron,因此加個參數改為使用 Chrome

# 指定使用Chrome測試
npx cypress run --browser chrome --spec "project/**"
# 使用headless模式測試
npx cypress run --headless --browser chrome --spec "project/**"

執行畫面

自動錄製

Cypress Studio

這是官方自己出的錄製功能,在 6.3.0 版本中發布,屬於實驗功能

實驗功能就是官方推出的試驗性功能,預設都是關閉的,必須手動開啟。實驗功能日後可能預設為開啟或整個移除

開啟方式為打開 cypress.json 輸入以下設定

"experimentalStudio": true

用 Google 搜尋的 Code 來演示,僅先瀏覽 Google 搜尋頁面

it('搜尋', () => {
cy.visit('https://www.google.com/?hl=zh_tw')
})

執行測試,開啟 Cypress Studio 功能後就可以在測試介面看到這個按鈕

按下之後會呈現這個頁面

之後開始執行你的操作,可以看到左邊已經開始記錄這些操作了

如果要捨棄這些操作就按下 Cancel,否則按下 Save Commands 即可,會自動把這些程式碼加到你的檔案中

以下是錄製結束後的檔案內容

it('搜尋', () => {
cy.visit('https://www.google.com/?hl=zh_tw')
/* ==== Generated with Cypress Studio ==== */
cy.get('.gLFyf').type('YouTube{enter}');
/* ==== End Cypress Studio ==== */
})

Cypress Recorder

去 Chrome 下載擴充功能 Cypress Recorder

打開想要測試的頁面,然後開啟 Cypress Recorder,按下 Start Recording

執行你想要測試的操作,結束後再次打開 Cypress Recorder

可以看到已經將你的操作側錄下來並且轉成程式碼,至於 Assert 部分則會自動幫你填入。當然,不見得是你要的,但是操作的部分應該沒問題

按下 Stop Recording 即可將程式碼複製下來 (Copy to Clipboard)

透過側錄得到的程式碼不見得是你完全要的,但錄製完後拿來編修還是很有效率的,至少操作過程不需每一行都手寫,透過 Recorder 可以有效加速開發速度

報表產出

除了透過文字命令看到的測試結果外,也可透過內建功能產出 xml 格式檔案報表。如果覺得內建的測試結果不夠用,需要產出報表的話,可以透過 Mochawesome

詳細可見 Cypress 官方網站教學文件:Reporters

安裝

npm install --save-dev mochawesome mochawesome-merge mochawesome-report-generator

準備

在 cypress.json 寫入設定

{
"reporter": "mochawesome",
"reporterOptions": {
"reportDir": "cypress/results",
"overwrite": false,
"html": false,
"json": true
}
}

執行

這會執行所有測試,並且將 json 格式測試報告產出在指定目錄 cypress/results 底下

cypress run --reporter mochawesome

當然也可以搭配其他指令,產出指定測試的報表

npx cypress run --reporter mochawesome --spec "project/testFile.js"

產出的 json 報告會根據不同測試檔案而產出多個檔案,檔案分佈可能如下

cypress/results/mochawesome.json, cypress/results/mochawesome_001.json, ...

透過指令將所有 json 檔案合併在一起

npx mochawesome-merge ./cypress/results/*.json -o output.json

將合併後的 json 檔解析成 html

npx marge mochawesome.json

成果如下圖,點擊測試項目也可以把測試程式碼展開

注意

透過 Cypress 官方網站文件指令產出的 json 會無法解析成 html,這似乎是 Windows 才有的 Bug。觀看官方文件時需特別注意

我文中使用的指令可以正常運作,那是我去閱讀套件本身的 GitHub 查到的

# 舊指令
npx mochawesome-merge "cypress/results/*.json" > mochawesome.json
# 會出現以下錯誤訊息
✘ Some files could not be processed:
mochawesome.json
Unexpected token � in JSON at position 0

GitHub 上相關的 Issues

Incorrect JSON coding format after mochawesome-merge #5103

Incorrect JSON coding format after mochawesome-merge #5111

Cypress 進階技巧

Config 設定 — Base Url

cypress.json 就是 Cypress 的 config 檔,就算沒使用額外套件也可以拿來做一些設定

例如說 baseUrl,可以將基本的 url 寫入,日後寫測試程式碼就可以省略那一段

cypress.json

{
"baseUrl": "http://localhost:1234/Web/",
}

測試檔.js

context('測試', () => {
cy.visit('/') // 前往 baseUrl
cy.visit('/Login') // 前往 http://localhost:1234/Web/Login
cy.visit('Login') // 前面不加斜線仍可運作
})

這個好處在於 Cypress 可以設定多個不同環境的 config 檔案。例如:Development、QA、Staging、Production

分別填入不同的 baseUrl 而測試程式碼只要填測試功能頁的網址即可,一套程式碼可以套用到所有環境上

檔案上傳

這可以透過 cypress-file-upload 達成

安裝

npm install --save-dev cypress-file-upload

設定

打開 cypress\support\commands.js 並加入一行

import 'cypress-file-upload';

這樣就可以開始使用這個套件的擴充語法了

假設網站中有一個按鈕可以讓用戶上傳圖片,且按鈕 ID 為 Upload,先將圖片放入 cypress\fixtures 並命名為 TestImage.png

使用 attachFile 語法上傳圖片

// 上傳圖片
cy.get('#Upload').attachFile('TestImage.png')

包裝方法 (Method)

有時候我們想將一些共用方法給包裝起來,而不要全部寫在測試程式碼內,這時可以將共用方法寫在 cypress\support\commands.js 內

其實打開這個檔案,就已經有寫好的範例了,看一下就知道怎麼寫

以下寫一個登入範例

Cypress.Commands.add('login', (account, password) => {
cy.request('POST', 'http://localhost:1234/Web/Login', {
account: account,
password: password
})
})

呼叫

cy.login('MyAccount', 'MyPassword')

從 JSON 檔案讀取資料

範例 JSON 檔,需將檔案放在 cypress\fixtures 底下

{
"name": "Hao",
"age": 26,
"gender": "Male"
}

使用 fixture 語法載入 JSON,並將其命名為 data

cy.fixture('Data.json').as('data')

使用裡面的資料

let name = this.data.name
let age = this.data.age
let gender = this.data.gender

需特別注意的是一般來說測試程式碼的 it 都是這樣寫

it('Test', () => {
// ...
})

但是為了接觸到 fixture 的資料,必須使用 function callbacks,否則測試引擎會認不得 this 這個關鍵字

請注意 it 後面的寫法不同

it('Test', function () {
// ...
})

其他

在使用資料前先修改讀取進來的資料

cy.fixture('data').then((data)  => {
data.name = 'Jane'
// 拿資料去做其他事...
})

多環境測試

說明

如果要測試本地端、測試區、Staging 區、正式區等環境,一般來說只要更改 Base Url 即可達到這個效果。但如果要根據不同環境做不同行為的測試、或是測試區與正式區資料不一致的情況,那就有必要針對不同環境使用不同的測試資料

詳細可參閱 Configuration API

設定

在 cypress\plugins\index.js 中,加入以下程式碼

// promisified fs module
const fs = require('fs-extra')
const path = require('path')
function getConfigurationByFile(file) {
const pathToConfigFile = path.resolve('cypress', 'config', `${file}.json`)
return fs.readJson(pathToConfigFile)
}
// plugins file
module.exports = (on, config) => {
// accept a configFile value or use development by default
const file = config.env.configFile || 'development'
return getConfigurationByFile(file)
}

這樣預設的 config 環境就會是 development.json

在 cypress\config 中,新增以下檔案

  1. development.json
  2. qa.json
  3. staging.json
  4. production.json

此時只要將檔案以 json 格式撰寫想要填入的參數即可,外層需包覆一層 “env”

也可以根據不同環境設定不同的 baseUrl,這邊的 baseUrl 設定會覆蓋掉 cypress.json 的 baseUrl

// cypress/config/development.json{
"baseUrl": "http://localhost:1234/Web/",
"env": {
"ENVIRONMENT": "development",
"Account": "TestAccount"
}
}

取用資料

// 取用資料
let account = Cypress.env('Account')
// 根據不同環境做不同事
if (Cypress.env('ENVIRONMENT') === 'development') {
// ...
}

測試情境題

被測試的網站需要先登入才能測試

我們先思考一下,該怎麼進入測試頁?為什麼要思考?因為在進入前,需要先經過登入這一步。那難道我們測試每個功能、每個頁面之前都要先跑一次登入嗎?

沒錯,每次都要登入,但不必透過 UI 來登入

許多測試框架都可以使用 Hooks,就是在測試前/後要執行什麼步驟。我們在進入測試頁前先執行一次登入,就可以進入測試頁開始測試了

使用 beforeEach 在每個測試前都先執行登入,至於怎麼執行?只要看我們的程式碼直接找到進入點並且 POST 資料進去就可以了

beforeEach(() => {
cy.request('POST', 'http://localhost:1234/Web/login', {
account: 'Hao',
password: '123456'
})
})

當然前面有一個範例是把登入進行包裝,也可以透過那個包裝好的方法進行登入

beforeEach(() => {
cy.login('Hao', '123456')
})

想要隱藏部分資訊,讓測試程式更好寫

以登入為例,如果測試常用的登入帳號就是那一組,那就不需要帶參數進去,直接寫死就好了

Cypress.Commands.add('login', () => {
cy.request('POST', 'http://localhost:1234/Web/Login', {
account: 'Hao',
password: '123456'
})
})

這樣呼叫起來也方便

cy.login()

但是如果不同帳號有不同權限,在少數情況下需要使用別的帳號登入時怎麼辦?只要利用之前提到的多環境測試技巧,把參數包裝起來即可

以下檔案為 development.json

{
"env": {
"ENVIRONMENT": "development",

"login_url": "http://localhost:1234/Web/Login",
"Account": "Hao",
"Password": "123456"
}
}

登入方法改寫為

Cypress.Commands.add('login', (account, password) => {
let _account = Cypress.env('Account')
let _password = Cypress.env('Password')

if (account != null){
_account = account
}
if (password != null){
_password = password
}

cy.request('POST', Cypress.env('login_url'), {
userName: _account,
paw: _password
})
})

這樣只要有帶入參數的時候就會改用那組帳號密碼,否則就是預設寫好的帳號密碼

這個技巧同時也解決了根據不同環境需使用不同帳號密碼的問題

Protractor

分析

  1. 一開始是針對 Angular 設計的 E2E 框架,由 Google 維護

  1. GitHub 上長時間沒有更新,最近的更新是支援新版 TypeScript,似乎對社群的 Issues 實現較少
  2. 對於非 Angular 的頁面,需加上設定,否則程式碼會寫得很冗長

說明

安裝

npm install -g protractor
webdriver-manager update

準備

先將 WebDriver 打開

webdriver-manager start

執行測試至少需要 2 個檔案,設定檔 conf.js 以及測試檔案

// conf.jsexports.config = {
framework: 'jasmine',
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['spec.js']
}
// spec.jsit('搜尋', function() {
// 如果不是Angular頁面則必須寫這行,否則程式碼須寫得很長
browser.waitForAngularEnabled(false);

browser.get('https://www.google.com/?hl=zh_tw');

element(by.name('q')).sendKeys('YouTube');
// 這邊會跳提示說有2個元素,Protractor會自動使用第1個而非報錯
element(by.name('btnK')).click();
expect(browser.getTitle()).toEqual('YouTube - Google 搜尋');
});

執行

protractor conf.js

Headless 模式

需在 conf.js 加入設定

// conf.jsexports.config = {
framework: 'jasmine',
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['spec.js'],

// 多了以下這段
capabilities: {
browserName: 'chrome',
chromeOptions: {
args: [ "--headless"]
}
}
}

非 Angular 頁面測試

如前面範例所述,加上一行就可以了。但如果不加那一行,會需要寫得很冗長

// spec.jsit('搜尋', function() {
// browser.waitForAngularEnabled(false);

browser.driver.get('https://www.google.com/?hl=zh_tw');

browser.driver.findElement(by.name('q')).sendKeys('YouTube');
browser.driver.findElement(by.name('btnK')).click(); expect(browser.driver.getTitle()).toEqual('YouTube - Google 搜尋');
});

執行畫面

記得要先另開視窗執行 WebDriver。圖中開了 2 個 CMD 視窗,左邊執行 WebDriver,右邊執行測試

Selenium

分析

  1. 支援多種語言開發,如 JavaScript、Java、C#、Python、R 等……
  2. 支援多種瀏覽器測試,如 Chrome、Firefox、IE、Safari、Opera 等……
  3. 擁有強大的管理工具 Selenium Grid,可以將測試分散給不同機器執行以增加速度,也可針對不同機器執行不同瀏覽器測試
  4. 支援多瀏覽器同時執行測試

  1. 以 JavaScript 以外的語言開發測試時,不便管理瀏覽器的核心版本。以 C# 為例,透過 NuGet 抓取核心會有更新太慢的問題
  2. 沒有內建的 Assert ,需使用 Node.js 內建的 Assert 來做判斷。只有 Assert 出錯了才會報錯,看不到有多少測試、通過幾個、失敗幾個

說明

安裝

npm install selenium-webdriver

準備

下載想要測的瀏覽器的 Driver,這裡用 Chrome 做示範

ChromeDriver 下載

將下載來的 Driver 放到與測試檔案同個目錄底下,或是設置環境變數也可以

測試檔案

// Google.jsconst {Builder, By, Key, until} = require('selenium-webdriver');
(async function example() {
let driver = await new Builder().forBrowser('chrome').build();
try {
await driver.get('https://www.google.com/?hl=zh_tw');
await driver.findElement(By.name('q')).sendKeys('YouTube', Key.ENTER);var title = await driver.getTitle();;const assert = require("assert");
assert.equal('YouTube - Google 搜尋', title);
}
finally{
driver.quit();
}
})();

執行

# node 檔名
node Google

執行畫面

沒有任何報告產出

Nightwatch

分析

  1. 語法較其他測試框架不同,需重新適應

說明

安裝

npm install nightwatch --save-dev
npm install selenium-server --save-dev

準備

創建 config 檔案

// nightwatch.conf.jsmodule.exports = {
// An array of folders (excluding subfolders) where your tests are located;
// if this is not specified, the test source must be passed as the second argument to the test runner.
src_folders: [],
webdriver: {
start_process: true,
port: 4444,
server_path: require('chromedriver').path,
cli_args: [
]
},
test_settings: {
default: {
launch_url: 'https://nightwatchjs.org',
desiredCapabilities : {
browserName : 'chrome',
}
}
}
};

範例

// Google.jsmodule.exports = {
'搜尋' : function(browser) {
browser
.url('https://www.google.com/?hl=zh_tw')
.waitForElementVisible('body')
.assert.visible('input[name=q]')
.setValue('input[name=q]', 'YouTube')
.assert.visible('input[name=btnK]')
.click('input[name=btnK]')
.assert.titleContains('YouTube - Google 搜尋')
.end();
}
};

執行

npm test Google.js# 內建測試範例
npm test ./node_modules/nightwatch/examples/tests/ecosia.js

執行畫面

其他

最近得知微軟有開源一款以 Python 寫的 E2E 測試工具 Playwright

不過這我就沒研究了,可能改天有空來試試看吧。目前文中提到的 4 種框架我覺得 Cypress 是最好用的

參考

什麼是Selenium WebDriver ?

Comparison between Selenium, Protractor, Cypress, and WebdriverIO

鼠年全馬鐵人挑戰 WEEK 10:Selenium Grid

how to use Protractor on non angularjs website?

cypress-file-upload

The Most Common Selenium Challenges

Cypress vs Selenium WebDriver: Better, or just different?

Difference between in-process and out-process service

DOM event

聯絡我

Facebook: 陳皓軒

LinkedIn: 陳皓軒

--

--