本篇內容將記錄並介紹使用Puppeteer進行自動化網頁測試,並依靠約定來避免反覆修改測試用例的方案。主要解決頁面衆多時,修改代碼致使的牽連錯誤沒法被發現的運行時問題。文章首發於 我的博客。
對前端感興趣但願一塊兒討論的能夠加我vx:w554091944
目前咱們在持續開發着一個幾十個頁面,十萬+行代碼的項目,隨着產品的更迭,總會出現這樣的問題。在對某些業務邏輯或者功能進行添加或者修改的時候(尤爲是通用邏輯),這些通用的邏輯或者組件每每會牽扯到一些其餘地方的問題。因爲測試人員受限,咱們很難在完成一個模塊單元后,對全部功能從新測試一遍。
同時,因爲環境及數據的區別,(以及在開發過程當中對代碼完備性的疏忽),代碼會在某些特殊數據的解析和和展現上出現問題,在開發和測試中很難去發現。總的來講,咱們但願有一個這樣的工具,幫咱們解決上述幾個問題:前端
其中,最重要的問題,就是將測試代碼與功能解耦,避免每次迭代和修改都須要追加新的測試用例。咱們如何作到這一點呢?首先咱們來梳理下測試平臺的功能。git
因爲咱們的平臺主要是進行數據展現,因此咱們在測試過程當中,主要以平常的展現數據爲重心便可,針對一些複雜的表單操做先不予處理。針對上述的幾個問題,咱們針對自動化測試工具的功能以下:github
根據以上的梳理,咱們能夠把整個應用分爲幾個測試單元segmentfault
經過這樣的劃分,咱們針對各個單元進行具體的測試邏輯書寫用例,這樣就能夠避免再添加新功能和頁面時,頻繁對測試用例進行修改了。api
帶着上面咱們的需求,咱們來看下Puppeteer的功能和特性,是否可以知足咱們的要求。cookie
文檔地址網絡
Puppeteer是一個Node庫,它提供了一個高級 API 來經過 DevTools 協議控制 Chromium 或 Chrome。Puppeteer 默認以 headless 模式運行,可是能夠經過修改配置文件運行「有頭」模式。less
咱們可使用Puppeteer完成如下工做:dom
咱們來經過一些小案例,來介紹他們的基本功能:async
puppeteer能夠建立page實例,並使用goto方法進行頁面訪問,page包含一系列方法,能夠對頁面進行各類操做。
(async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); // ba認證 await page.authenticate({ username, password }); // 訪問頁面 await page.goto('https://example.com'); // 進行截圖 await page.screenshot({path: 'example.png'}); await browser.close(); })();
首先,對於SPA(單頁面應用),咱們都知道,當頁面進入後,客戶端代碼纔開始進行渲染工做。咱們須要等到頁面內容渲染完成後,再進行對應的操做。咱們有如下幾種方法來使用
puppeteer針對頁面的訪問,切換等,提供了waitUntil參數,來肯定知足什麼條件才認爲頁面跳轉完成。包括如下事件:
經過waitUnitl,咱們能夠當頁面請求都完成以後,肯定頁面已經訪問完成。
waitFor方法能夠在指定動做完成後才進行resolve
// wait for selector await page.waitFor('.foo'); // wait for 1 second await page.waitFor(1000); // wait for predicate await page.waitFor(() => !!document.querySelector('.foo'));
咱們能夠利用waitForSelector方法,當登陸框渲染成功後,才進行登陸操做
// 等待密碼輸入框渲染 await page.waitFor('#password'); // 輸入用戶名 await page.type('input#username', "username"); // 輸入密碼 await page.type('input#password', "testpass"); // 點擊登陸按鈕 await Promise.all([ page.waitForNavigation(), // 等跳轉完成後resolve page.click('button.login-button'), // 點擊該連接將間接致使導航(跳轉) ]); await page.waitFor(2000) // 獲取cookies const cookies = await page.cookies()
主要利用到page實例的選擇器功能
const table = await page.$('.table') const links = await table.$$eval('a.link-detail', links => links.map(link => link.href) ); // 循環訪問links ...
puppeteer能夠監聽在頁面訪問過程當中的報錯,請求等等,這樣咱們就能夠捕獲到頁面的訪問錯誤並進行上報啦,這也是咱們進行測試須要的基本功能~
// 當發生頁面js代碼沒有捕獲的異常時觸發。 page.on('pagerror', () => {}) // 當頁面崩潰時觸發。 page.on('error', () => {}) // 當頁面發送一個請求時觸發 page.on('request') // 當頁面的某個請求接收到對應的 response 時觸發。 page.on('response')
經過以上的幾個小案例,咱們發現Puppeteer的功能很是強大,徹底可以知足咱們以上的對頁面進行自動訪問的需求。接下來,咱們針對咱們的測試單元進行個單元用例的書寫
經過咱們上面對測試單元的規劃,咱們能夠規劃一下咱們的測試路徑
訪問網站 -> 登錄 -> 訪問頁面1 -> 進行基本單元測試 -> 獲取詳情頁跳轉連接 -> 依次訪問詳情頁 -> 進行基本單元測試
-> 訪問頁面2 ...
因此,咱們能夠拆分出幾個大類,和幾個測試單元,來進行各項測試
// 包含基本的測試方法,log輸出等 class Base {} // 詳情頁單元,進行一些基本的單元測試 class PageDetal extends Base {} // 頁面單元,進行基本的單元測試,並獲取並依次訪問詳情頁 class Page extends PageDetal {} // 進行登陸等操做,並依次訪問頁面單元進行測試 class Root extends Base {}
同時,咱們如何在功能頁面變化時,跟蹤到測試的變化呢,咱們能夠針對咱們測試的功能,爲其添加自定義標籤test-role,測試時,根據自定義標籤進行測試邏輯的編寫。
例如針對時間切換單元,咱們作一下簡單的介紹:
// 1. 獲取測試單元的元素 const timeSwitch = await page.$('[test-role="time-switch"]'); // 若頁面沒有timeSwitch, 則不用進行測試 if (!timeSwitch) return // 2. time switch的切換按鈕 const buttons = timeSwitch.$$('.time-switch-button') // 3. 對按鈕進行循環點擊 for (let i = 0; i < buttons.length; i++) { const button = buttons[i] // 點擊按鈕 await button.click() // 重點! 等待對應的內容出現時,才認定頁面訪問成功 try { await page.waitFor('[test-role="time-switch-content"]') } catch (error) { reportError (error) } // 截圖 await page.screenshot() }
上面只是進行了一個簡單的訪問內容測試,咱們能夠根據咱們的用例單元書寫各自的測試邏輯,在咱們平常開發時,只須要對須要測試的內容,加上對應的test-role便可。
根據以上的功能劃分,咱們很好的將一整個應用拆分紅各個測試單元進行單元測試。須要注意的是,咱們目前僅僅是對頁面的可訪問性進行測試,僅僅驗證當用戶進行各類操做,訪問各個頁面單元時頁面是否會出錯。並無對頁面的具體展現效果進行測試,這樣會和頁面的功能內容耦合起來,就須要單獨的測試用例的編寫了。