網頁在提測流轉給 QA 後,如何能幫他們更有效而準確的完成測試,是我一直在思考的一個問題。
QA 他們會對網頁編寫測試用例,在提測之前會讓我們將優先順序最高的用例跑通,這在一定程度上能夠避免頻繁的返工,保證測試的順暢。
自己之前想過做 UI 的單元測試,一有修改就跑一遍用例,但是維護成本太高,並且每次留給我們的開發時間並不多。
最近在看多份測試記錄的 BUG 單中發現,45%~70% 之間的 BUG 都是內容性問題,例如網頁中缺了個字、少了段話、圖片呈現不對等問題。
這些用肉眼看,其實很容易辨別。只要有個工具,能呈現不同操作下的網頁,就可以大大提升 QA 的驗收效率。
於是就想到了 Puppeteer(傀儡師),它是一個 Node.js 庫,提供了一套 API 控制 Chrome 或 Chromium 瀏覽器,可生成頁面快照截圖、模擬使用者行為等。
Chromium 是 Chrome 的開源版本,兩者在介面、功能等方面會存在些區別。
雖然 Puppeteer 可以模擬使用者行為,但是它不能模擬使用者在使用者端 WebView 中的行為,例如點選頭像進入使用者使用者端中的主頁。
簡單理解,就是 Puppeteer 能呈現 Chrome 瀏覽器中的頁面,但如果要測試使用者端的樣式相容性和各種互動行為,就無法實現了。
而內容展示異常的問題,無論是在哪一端,都能表現一致,因此可以用 Puppeteer 來作為自動化測試工具。
當前 Puppeteer 的最新版本是 19.2.2,API 分為 20 多個模組,可查詢頁面 DOM 元素、螢幕截圖、處理 WebSocket、模擬行動端裝置、植入指令碼、請求攔截等操作。
我當前的目標是檢視頁面內容,所以在頁面中也需要有些互動。
先基於 KOA,快速搭建一個新專案,因為 Puppeteer 庫比較大,所以為了不影響其他專案的構建和執行,就單獨開了個專案。
再引入相關的 Node 庫,fs 用於建立目錄,path 用於拼接絕對路徑,config 用於讀取組態檔。
const puppeteer = require('puppeteer'); const fs = require('fs'); const path = require('path'); const config = require('config'); const domain = config.get('domain');
接著是宣告一條路由,例如用 get 方法存取 test/game,傳遞型別和環境。
router.get("/test/game", async (ctx) => { const { type, env = "www" } = ctx.query; let data = []; switch (type) { case "1": data = await hot(env); break; } ctx.body = { code: 0, data }; });
hot() 函數就是自動化測試的主體,核心邏輯其實就是點選某一欄,呈現頁面後,再截一張圖。
async function hot(env) { const savePath = path.resolve(__dirname, "../static/hot"); // 將相對路徑轉換成絕對路徑 const iPhone = puppeteer.KnownDevices["iPhone 6"]; // 模擬iPhone 6 // 當指定目錄不存在時,就將其建立,Sync 字尾表示同步,recursive 參數列示遞迴建立 !fs.existsSync(savePath) && fs.mkdirSync(savePath, { recursive: true }); // 要返回的圖片路徑 const paths = []; /** * 本地環境可以不需要設定 args * 但是在 Linux 伺服器中在啟動 puppeteer 時,預設要帶上 --no-sandbox 引數 */ return puppeteer.launch({ args: ["--no-sandbox", "--disable-setuid-sandbox"] }).then(async (browser) => { const page = await browser.newPage(); // 建立新的瀏覽器上下文 await page.emulate(iPhone); // 模擬裝置 // 預設主頁 await page.goto(`https://${env}.xxx.com/game/hot.html`, { waitUntil: "networkidle0" // 等待到沒有網路請求 }); // 截圖 await screenshot(page, `${savePath}/1.png`); paths.push(`${domain}/static/hot/1.png`); // 查詢符合樣式的選單欄 const tabs = await page.$$(".green_calss"); // 點選第二個選單欄 tabs[1].tap(); await page.waitForNetworkIdle(); await screenshot(page, `${savePath}/2.png`); paths.push(`${domain}/static/hot/2.png`); // 點選第三個選單欄 tabs[2].tap(); await page.waitForNetworkIdle(); await screenshot(page, `${savePath}/3.png`); paths.push(`${domain}/static/hot/3.png`); await page.close(); await browser.close(); return paths; }); }
path.resolve()、fs.existsSync() 和 fs.mkdirSync() 都是 Node.js 提供的方法。
page.goto() 可跳轉到指定 URL 的網頁,networkidle0 可等待到頁面無網路請求。
在自動化測試時,需要有個頁面完成的時間點,如果用延時的方式,會不太準確,所以就想到了網路請求。
我們這邊的網頁以行動端居多,所以需要選擇要模擬的裝置,例如 iPhone 6。
page.$$() 是 ElementHandle 提供的一個方法,相當於 Document.querySelectorAll() 方法。
tabs[1].tap() 就是模擬使用者觸控式螢幕幕,另外支援的事件還包括 click()、drag()、focus() 等。
screenshot() 是一個截圖函數,內部呼叫 page.screenshot() 方法,可擷取完整頁面,包括捲動區域。
async function screenshot(page, savePath) { await page.screenshot({ path: savePath, type: "png", fullPage: true //邊捲動邊截圖 }); }
前端介面目前比較簡潔,就一個活動下拉框和一個環境下拉框,以及一個提交按鈕。
提交後,就會從後臺拿到頁面快照,這些快照都會按照寫好的指令碼生成。
1)遇到的問題
在實際使用中注意到,如果頁面內包含一塊定高區域,那麼隱藏區域就需要捲動後才能呈現。
當網頁中有一個資源阻塞載入時,若是 waitUntil 引數的值是 networkidle0,那頁面就會報超時的錯誤。
await page.goto(url, { waitUntil: ["networkidle0"] });
但如果 goto() 方法不加等待時機,那就會一直等下去,沒有結果。
await page.goto(url);
可以改成 networkidle2,networkidle0 是指在 500ms 內沒有任何網路連線,而 networkidle2 是指網路連線個數不超過 2 個。
在部署到伺服器後,由於字型的問題,頁面內的中文會出現亂碼,解決辦法就是給伺服器安裝中文字型。
參考資料:
Automating Google Chrome with Node.js