圖片來自 Getty Images

Web 測試框架 Playwright

本文轉載自 Leon 的網誌

對於 web 自動化測試,一路走來我們用過許多方案,剛開始是用最多人知道的 Selenium,那是前端框架還不盛行的時代,也是要手動寫測試腳本的時代。

手動寫測試腳本這件事對工程師們來說,是繁重又缺乏創造力的工作,當時流行的 jQuery web 元件讓「抓 id / class」變成一件相當繁瑣的事,而最大的問題是人力的耗損,算式很簡單:會打碼 = 貴,任何有成本概念的人都不會想把人力投放在測試上。(投放在測試上/投資在品質上,是兩個不同的概念,不要混為一談。)

第二階段,我們找到了 Sikuli,它是以比對螢幕圖像與位置為基礎的測試工具,也有寫與錄腳本的能力,操作也夠親切,只要有基礎的程式與邏輯概念的人都可以無痛寫(錄)出所有實境操作的劇本,但 Sikuli 的問題是只要換個解析度或換個作業系統,導致 web 元件位置變化或者 render 上的變化,那測試就過不了,只能重寫(錄)…,好在當時我們的 POS 的螢幕解析度都是固定的。

但是 POS 之外的專案,由於上面提到的問題,Sikuli 就不足以應付了,於是我們又看到了 Katalon。Katalon 是以 Selenium 為基礎的產品,它的錄製工具會自動幫我們抓網頁元素的 ID,省去了部份的抓 ID 工作,但對於複雜的元件(如 combo box、data grid)還是需要事後人工編修,但整體來說還是個可接受的解決方案。

而今天我們的新玩具叫做 Playwright,Playwright 是微軟開發的 web 自動化測試工具,它有幾項值得一提的特性:

  • 跨平台,macOS、Linux、Windows 皆可用。
  • 跨瀏覽器,可操控 WebKit、Firefox、Chromium 三大瀏覽器。
  • 跨語言,Playwright 原本是以 Node.js 開發,後來微軟陸續移植到 Python、Java 和 .NET 上,雖然語法不同但有著相似的 API。
  • 完整的工具鍊,Playwright 包括 Playwright 與 Playwright Test Runner 兩部份,想要拆開來用也可以。
  • 年輕、開發活躍、有富爸爸支持,微軟自己也在用。

也有幾項採用前得注意的點:

  • 不能操控手機,只能開啟瀏覽器的手機模擬模式,但我們都知道,真的和模擬的有部份的差異。
  • 它的 WebKit 瀏覽器和 Safari 也不一樣,雖然 Safari 底層也是 WebKit,但一樣會有點差異。

碰 Playwright 前的前置作業

開一個空的 Node.js 專案,資料夾架構如下:

.
├─node_modules
├─tests
├─package.json
└─package-lock.json

所有的測試腳本都放在 tests/ 裡面,Playwright 會去跑 test/ 與旗下資料夾內所有 *.spec.js 與 *.spec.ts 的測試腳本。

安裝

Playwright 在 NPM 分為兩個主要套件:

  • playwright:Playwright 套件
  • @playwright/test:Playwright Test Runner 套件

兩者的 API 用法略有差異,依 Playwright 自己的文件說,Playwright Test Runner 比較適合 end-to-end testing 的場景,這也是我們的應用場景,所以下文我們的 Playwright 都是指 Playwright Test Runner。

安裝 Playwright Test Runner:

npm install --save-dev @playwright/test

Playwright Test Runner 並不使用我們的瀏覽器,它有自帶瀏覽器,用這行指令把瀏覽器裝起來:

npx playwright install

一行搞定三大瀏覽器,因此我們也不需要配置任何的瀏覽器路徑等等。:)

基礎使用與配置

用錄的產生腳本

我們用錄的來上手:

npx playwright codegen wikipedia.org --output ./tests/wikipedia.spec.js

跳出瀏覽器和 Playwright Inspector,裡面有錄出來的腳本:

Playwright

隨便點幾下之後,那個 wikipedia.spec.js 長這樣:

const { test, expect } = require('@playwright/test');

test('test', async ({ page }) => {
  // Go to https://www.wikipedia.org/
  await page.goto('https://www.wikipedia.org/');

  // Click text=中文
  await page.click('text=中文');
  expect(page.url()).toBe('https://zh.wikipedia.org/wiki/Wikipedia:%E9%A6%96%E9%A1%B5');

  // Check input[type="checkbox"]
  await page.check('input[type="checkbox"]');

  // Click a:has-text("臺灣正體")
  await page.click('a:has-text("臺灣正體")');
  expect(page.url()).toBe('https://zh.wikipedia.org/zh-tw/Wikipedia:%E9%A6%96%E9%A1%B5');

  // Click a:has-text("登入")
  await page.click('a:has-text("登入")');

  // Click text=中文(繁體)
  await page.click('text=中文(繁體)');

  // Click [placeholder="輸入您的使用者名稱"]
  await page.click('[placeholder="輸入您的使用者名稱"]');

  // Fill [placeholder="輸入您的使用者名稱"]
  await page.fill('[placeholder="輸入您的使用者名稱"]', 'jimmy');

  // Click [placeholder="輸入您的密碼"]
  await page.click('[placeholder="輸入您的密碼"]');

  // Fill [placeholder="輸入您的密碼"]
  await page.fill('[placeholder="輸入您的密碼"]', 'walls');

  // Click button:has-text("登入")
  await page.click('button:has-text("登入")');

  // Go to https://zh.wikipedia.org/wiki/Wikipedia:%E9%A6%96%E9%A1%B5
  await page.goto('https://zh.wikipedia.org/wiki/Wikipedia:%E9%A6%96%E9%A1%B5');
  expect(page.url()).toBe('https://zh.wikipedia.org/wiki/Wikipedia:%E9%A6%96%E9%A1%B5');
});

可以看到,錄出來的並非完美的,有好幾行多餘的敘述,而優點是非常簡單就能上手。

跑測試

要跑測試也很簡單:

npx playwright test --headed --browser=chromium

這行指令會跑所有在 test/ 與旗下資料夾內所有 *.spec.js 與 *.spec.ts 的測試腳本。

後面我們也用參數指定用 Chromium 瀏覽器的有頭模式跑測試。

如果要跑指定腳本,就指定一下:

npx playwright test tests/wikipedia.spec.js --headed --browser=chromium

用有頭模式跑測試時也可以叫出 Playwright Inspector:

# Linux/macOS
PWDEBUG=1 npm run test

# Windows with cmd.exe
set PWDEBUG=1
npm run test

# Windows with PowerShell
$env:PWDEBUG=1
npm run test

Playwright Inspector

Playwright Inspector 可以手動控制腳本的進度,方便我們對測試腳本 debug。(那我們需要測試腳本的測試腳本嗎?)

配置

配置檔放在專案根目錄下,檔名可以是 playwright.config.js 或 playwright.conifg.ts,目前我的配置很陽春如下:

const { devices } = require('@playwright/test')

module.exports = {
    reporter: [
        ['list'],
        ['json', { outputFile: 'test-results/results.json' }],
        ['junit', { outputFile: 'test-results/results.xml' }],
    ],
    use: {
        baseURL: 'http://localhost:63800',
        headless: false,
        slowMo: 10,
        screen: { width: 1100, height: 700 },
        viewport: { width: 1100, height: 700 },
        
        // Artifacts
        screenshot: 'on',
        trace: 'only-on-failure',
        video: 'on',
    },
};

其中 reporter 內設定了三組 reporter,不同的 reporter 代表不同的輸出格式,list report 讓我們方便在 console 看到測試結果的摘要,而另外兩個則分別把測試結果輸出成 JSON 和 JUnit 的結構化格式,方便整合至其它系統。

sloMo 則是讓測試的每個步驟停頓的時間,單位是 1/1000 秒,年紀大了要跑慢一點。

screenshottracevideo 用於配置測試期間的抓圖、錄影和測試軌跡紀錄的行為,可以是「不論成功失敗都不存檔」、「不論成功失敗都存檔」、「只有失敗才存檔」。

Playwright 還有其它多如牛毛的配置參數,請參閱 Playwright 文件。

Trace

跑完測試的 trace 是完整的測試軌跡紀錄(也是最肥的),用 Playwright Trace Viewer 可以完整重現測試的所有軌跡:

npx playwright show-trace trace.zip

Playwright Trace Viewer

Expect API

Playwright 用 JEST 的 Expect 函式庫來驗證測試的條件,這部份也請參閱 Playwright 和 JEST 的文件。

使用技巧

這邊分享一些自己用到的或網路上挖到的小技巧。

等元素出現

前端框架已經是 web app 的主流方案,網頁元件大量使用 fetch,特別是資料型的表格,這種表格我們叫 data grid,Playwright 要操作 data grid 內的元素必須等待 fetch 的結果顯示出來,雖然 Playwright 有 auto-waiting 的機制,但也不是萬靈丹,對那些不是由用戶觸發的 event,auto-waiting 就不太靈光,還是要自己處理等待的機制。

Playwright 有提供好幾種 wait 函式,最原始的可以用 waitForTimeout(),但粗暴地用秒數等待大法還是有可能遇到時間到了,fetch 還沒收到資料的問題,我們可以改用另一個 waitForFunction() 來自定一個等待函式,等到元素出現才往下走:

await page.waitForFunction(() => document.querySelectorAll('.data-grid row').length >= 1);

上面的匿名函式的部分我們定義了至少 data grid 內要有一列資料,滿足條件後,watiForFunction() 才結束等待。

使用心得

Playwright 有內建 auto-waiting 的機制,用於因應現代化的 SPA 頻繁存取後端 API 的特性,以往的 Selenium 都要在腳本手動寫下 wait 來確保前端收到回應後再跑下一步,而 Playwright 的 auto-waiting 的機制會自動偵測點擊之類會發送請求的事件,並自動等到有回應的時候再跑下一步…,但以上只是理想而已,實際操練一天下來,還是發現有需要手動定義 wait 的場景。

另外和 Selenium 或 Katalon 相似的是「抓 id / class」的負擔依舊存在,並沒有變輕鬆的感覺。

結語

本文僅粗淺的介紹了 Playwright Test Runner 最基本的用法,特別是 Playwright 自己的函式庫和 JEST Expect 函式庫更是偷懶的隻字未提,這部份的用法取決於各個專案自己的測試情境,只能請讀者大大自行參閱原始文件了。

作者:Leon

不是五小編也不是七小編,就是六小編。

本文由 INFOLINK 聯騰資訊股份有限公司提供,聯騰資訊專注於為零售與餐飲產業提供智慧化的系統解決方案,以 ERP 為核心為客戶開拓 E 化應用,與 POS、BI、EC 等應用實現無縫整合,我們在此分享我們對產業與技術的觀點,歡迎與我們交流或追蹤我們。