Puppeteer E2E測試入門

黑金團隊發表於2018-11-29

本文內容涉及ES6 asyncjest的相關知識,對於以上內容不太瞭解的讀者可以先了解相關內容。

Puppeteer是什麼

它由Chrome官方團隊提供,通過Devtools協議在Node層提供了一系列API來控制chrome或者chromium,也就是說我們能夠編寫Node環境的程式碼即可對瀏覽器的行為進行控制。通過它我們可以做到以下行為:

  • 生成頁面快照:圖片、pdf

  • 抓取spa應用生成預渲染頁面

  • 自動化表單提交、UI測試、鍵盤輸入

  • 抓取應用的效能資料(chrome performance timeline)

  • 測試chrome擴充套件

當然以上行為都是puppeteer能力中極小的一部分,更多的讀者可以閱讀Puppeteer的文件來了解,本文將介紹Puppeteer在E2E測試的實踐

E2E測試

簡單來說,就是模擬真實使用者使用場景進行測試,預期應用能夠正常響應使用者的操作,其關鍵點在於模擬使用者使用環境,模擬使用者操作。

那對於Web應用來說,使用者環境就是瀏覽器,使用者操作主要是移動、點選,這些就是我們需要模擬的部分,下面就直接進入環境和實踐部分。

環境部分

本文的例子採用puppeteer jest jest-puppeteer 在mac環境實現

首先需要安裝以上依賴,這裡需要注意以下問題:

  • puppeteer會預設下載chromium,這裡可以通過export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true跳過chromium下載

  • Node版本最好使用大於8的版本

接下來說明一下相關的配置

首先是jest相關的配置,在根目錄建立jest.config.js

//jest.config.js
const config = require('config');
const _ = require('lodash');

module.exports = {
    preset: 'jest-puppeteer', //呼叫preset
    globals: _.assign({}, config.get('e2e.variable'), { //這裡可以注入全域性變數
        ENV_URL: config.get('baseUrl')
    }),
    testMatch: ['**/__e2e__/**/*.test.js?(x)'] //指定需要進行測試的檔案
};
複製程式碼

接下來就是配置puppeteer,在根目錄建立jest-puppeteer.config.js

//jest-puppeteer.config.js
module.exports = {
    launch: {
      	headless: true, //設定執行模式,false的情況下將會工作在有GUI介面的模式,true則不開啟GUI介面
        executablePath: //設定本地Chrome路徑,官方推薦使用Chrome Canary
            '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
    }
};
複製程式碼

實踐部分

環境已經配置好了,是時候進入實踐環節了。對於絕大部分系統尤其是管理系統來說,第一步要做的肯定是登入系統,因此,我們的第一個實踐就是用puppeteer指令碼模擬登入,這裡的例子基於作者這邊的一個系統的登入流程。

const loginFunc = page => async () => {
    await page.setViewport({ width: 1280, height: 720 });                           //設定視窗大小
    await page.goto(`${ENV_URL}/login`);                                            //前往登入地址
    const loginIframe = page.frames().find(f => f.name() === 'login-iframe');       //找到登入iframe
    await loginIframe.waitForSelector(
        '#login-tab-container > div.tab-item.tab-right'
    ); 
    await loginIframe.click('#login-tab-container > div.tab-item.tab-right');       //切換登入TAB
    await page.waitFor(1000);                                                       //特殊用途,之後說明
    const username = await loginIframe.waitForSelector('#username');                //找到輸入框
    await username.type(USERNAME);                                                  //輸入使用者名稱
    const pass = await loginIframe.waitForSelector('#password'); 
    await pass.type(PASSWORD);                                                      //輸入密碼
    await loginIframe.click('#login-btn');                                          //點選登入按鈕
    await page.waitForNavigation();                                                 //等待跳轉導航完成
};

module.exports = {
    loginFunc: loginFunc
};
複製程式碼

在這個例子中,展示了開啟登入頁面,輸入使用者名稱、密碼並進行登入的流程,主要涉及到了setViewport goto frames waitForSelector click waitFor type waitForNavigation這些API,下面說說其中幾個比較重要的API,更多的可以參考官方文件;

setViewport
設定視窗大小,返回一個promise

waitForSelector 
引數為CSS Selector,返回一個promise,直到指定的元素出現才會resolve,超時後會reject

click 
點選某個元素,返回一個promise

type 
向某個元素輸入內容,返回一個promise

waitForNavigation 
URL改變時觸發,返回promise,導航結束時resolve,通過history api進行URL更改時也可以通過該方法等待導航結束

frames
可以獲取頁面中所有的iframe
複製程式碼

相信讀者看完這段例子後也能感受到puppeteer的API設計是十分語意化的,非常好懂。但是實際上這個例子是存在幾個踩坑點的,這裡說一下筆者從這個例子中收穫的經驗:

  • waitForSelector只有當目標Selector原本不存在DOM中才會生效,如果目標元素是通過display: none; visibility: hidden這類方式進行切換的話,這個方法是直接resolve的。

  • 如果這個切換過程中還存在動畫效果,那在這個動畫效果的過程中,無論是click還是type動畫過程中的元素,操作都是不會生效的,必須等到動畫結束,因此在本例子中還增加了waitFor(1000)這句程式碼來等待動畫的結束

至此我們已經完成了登入,接下來就可以和jest結合進行自動化測試了,先放出本部分的例子:

const utils = require('./utils');

jest.setTimeout(10000);

describe('e2e test', () => {
    beforeAll(utils.loginFunc(page));
    it('e2e test-1', async () => {
        const el = await page.waitForSelector(
            '#root > .services_C2FC97 > .ant-row > .ant-col-8:nth-child(1) > .service-card_C2FC97'
        );
        expect(await el.$eval('p', node => node.innerText)).toBe('test');       //獲取指定元素的innerText
    });
});
複製程式碼

在這個例子裡,我們先引入了上面編寫的loginFunc來作為每一個測試的前置條件,由於使用了jest-puppeteer,執行環境中會自動注入puppeteer的page和browser物件,因此可以直接呼叫;這個例子中測試的是首頁的一張功能卡片的標題是否為test;這個例子是很基礎的,不過相信通過上面那個登入的例子,讀者已經瞭解到編寫E2E測試指令碼的套路了。

不過這裡還有一點需要說明,由於puppeteer啟動時間和開啟網頁的耗時比較難以估計,需要通過jest.setTimeout(ms)來延長一個測試執行的時間,否則有可能因為執行超時導致失敗,下面是這個例子最終的執行結果:

yarn e2e
yarn run v1.9.4
$ cross-env NODE_ENV=test jest -c jest.config.js
Determining test suites to run...
DevTools listening on ws://127.0.0.1:54751/devtools/browser/7d8d289a-5335-4ffc-a65c-f7e98cb34de0
[1129/154120.584773:WARNING:spdy_session.cc(3152)] Received HEADERS for invalid stream 25
 PASS  __e2e__/demo.test.js (6.931s)
  e2e test
    ✓ e2e test-1 (1540ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        7.046s
Ran all test suites.
✨  Done in 10.72s.
複製程式碼

至此,入門教程就結束了,是否感覺到很像按鍵精靈?但實際上通過puppeteer我們可以實現更強大的一些控制,包括監聽網路請求,監聽頁面建立等等,基本上我們能在瀏覽器手動做到的,通過它提供的Node API也能夠做到,這部分內容歡迎讀者去閱讀文件進一步的進行探索;

那E2E測試究竟能為我們帶來什麼呢?在筆者看來,對於一個長期迭代的專案,隨著專案規模的擴大,功能的迴歸測試耗費的時間會不可避免的增加,在這種情況下,如果有自動化測試能夠幫我們進行部分測試,將大大提高我們的效率;同時通過定時測試任務可以更早的發現產品功能上存在的問題,從而保證產品能夠按時交付,這是它帶來的最大的價值。

最後,推薦給大家一個小工具puppeteer-recorder,這是一個Chrome外掛,可以方便的錄製測試指令碼,從而擺脫手動編寫指令碼的苦惱?(由於同源策略,該工具不支援iframe內的操作錄製)

@author: Monado

相關文章