概述
教學 E2E 測試(端對端測試, End-to-end testing)框架 Cypress 入門以及一些進階技巧、測試情境分享
並且與現在較流行的 E2E 測試框架做比較,對象有 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) 不友善
彈出式視窗分為以下三種
- Simple — 純粹的提示,只有一個 “確認” 按鈕,主要是讓用戶知道某些訊息
- Prompt — 會要求用戶輸入一些資訊,例如 E-mail
- 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 的執行速度會比較快
可參考
Cypress
分析
優
- 非常詳細的文件說明,且每個說明頁都附上影片
- 內建許多測試範例,範例同時具有每一步快照、程式碼。把範例看完就幾乎等於會使用 Cypress
- 優美易用的 UI
- 每一步測試都有快照,有助於開發與 Debug
缺
- 目前只支援基於 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
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 中,新增以下檔案
- development.json
- qa.json
- staging.json
- 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
分析
優
- 一開始是針對 Angular 設計的 E2E 框架,由 Google 維護
缺
- GitHub 上長時間沒有更新,最近的更新是支援新版 TypeScript,似乎對社群的 Issues 實現較少
- 對於非 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
分析
優
- 支援多種語言開發,如 JavaScript、Java、C#、Python、R 等……
- 支援多種瀏覽器測試,如 Chrome、Firefox、IE、Safari、Opera 等……
- 擁有強大的管理工具 Selenium Grid,可以將測試分散給不同機器執行以增加速度,也可針對不同機器執行不同瀏覽器測試
- 支援多瀏覽器同時執行測試
缺
- 以 JavaScript 以外的語言開發測試時,不便管理瀏覽器的核心版本。以 C# 為例,透過 NuGet 抓取核心會有更新太慢的問題
- 沒有內建的 Assert ,需使用 Node.js 內建的 Assert 來做判斷。只有 Assert 出錯了才會報錯,看不到有多少測試、通過幾個、失敗幾個
說明
安裝
npm install selenium-webdriver
準備
下載想要測的瀏覽器的 Driver,這裡用 Chrome 做示範
將下載來的 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
分析
缺
- 語法較其他測試框架不同,需重新適應
說明
安裝
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 是最好用的
參考
Comparison between Selenium, Protractor, Cypress, and WebdriverIO
鼠年全馬鐵人挑戰 WEEK 10:Selenium Grid
how to use Protractor on non angularjs website?
The Most Common Selenium Challenges
Cypress vs Selenium WebDriver: Better, or just different?
Difference between in-process and out-process service
聯絡我
Facebook: 陳皓軒
LinkedIn: 陳皓軒