基於jest和puppeteer的前端自動化測試實戰

前端測試現狀

常常聽到後端同窗說「單元測試」,前端寫過測試用例的有多少?答案是:並很少,爲何呢?兩個主要緣由html

一、前端屬於GUI軟件,瀏覽器衆多,兼容問題讓人頭大,用戶量有必定規模的瀏覽器包括:前端

  IE八、IE九、IE十、IE十一、chrome、FireFox、360瀏覽器、搜狗瀏覽器、QQ瀏覽器……node

要在這麼多瀏覽器上作幾輪測試並不容易react

二、前端界面變化快,不少時候界面比測試腳本迭代的更快,測試跟不上腳步,投入產出不成正比git

以上兩點致使前端測試不受重視,不少前端開發者可能工做數年仍未寫過單元測試github

英國的一個前端開發者作了一項前端測試工具調查發現,目前仍有43%的前端開發者沒有作過任何前端測試,這是現狀web

 

 

該不應寫前端測試,仍是得視項目狀況而定,通常標準的開源項目都會作單元測試,因此有必要了解一下前端測試大概是個什麼東西chrome


 

分類

前面一直說的是前端測試而不是單元測試,是由於前端不一樣於後端,前端是有界面的,測試應該分爲單元測試和集成測試npm

所謂單元測試,就是測試一個函數或某個代碼片斷,經過模擬輸入確保輸出符合預期後端

實例1:如下是一個完整的測試用例,用來測試函數sum是否按預期的計算兩個數字之和

const sum = (a,b) => { return a+b; } describe('分組測試描述',() => { test('test 1+1', () => { expect(1 + 1).toBe(2); }); })

解釋一下兩個關鍵字:

describe,做用是將test分組,影響 beforeEach/afterEach/beforeAll/afterAll四個方法的做用域,它有兩個參數

第一個參數就是分組描述,描述這個分組是幹嗎的

第二個參數是個回調函數,內部能夠有多個test,test的做用是聲明一個測試

test,做用就是聲明一個測試,有三個參數

第一個一樣是描述,描述測試內容

第二個也是回調,內部爲詳細測試內容

第三個是測試超時時間,默認爲5s鍾,作單元測試通常都是足夠的,集成測試通常都是不夠的,能夠用jest.setTimeout(timeout)方法修改全部test默認超時時間

集成測試,測的是一個功能模塊,好比用戶註冊功能,集成測試又包括UI測試,UI測試用於確保頁面正常渲染

集成測試徹底是用測試腳本去模擬用戶操做,好比打開瀏覽器、點擊註冊連接、輸入用戶名密碼、點擊註冊

UI測試怎麼確保頁面正常渲染?

兩種方式:像素級對比和快照

像素及對比,就是首先人肉確認頁面渲染正常,執行腳本對頁面截個圖,下次利用測試腳本截個圖跟上次的截圖的每一個像素自動進行對比,若是每一個像素都同樣,那麼測試經過

快照,這裏的快照不是截圖的意思,而是將頁面渲染後的DOM結構生成一個序列化的文本,下次再次生成一個序列化的DOM文本與之對比,若是內容徹底同樣,測試經過,作快照測試,必須保證屢次測試輸出快照老是一致的,然而在react中,model常常變化,這時就要用mock模擬函數返回固定數據確保model不變,mock功能在下文有介紹


 

 

主流庫 

流行的單元測試包括jest、mocha、jasmine、……

流行的集成測試庫包括puppeteer、casperJS、PhantomJS、……

jest的特色是零配置、即時反饋,它全部測試用例默認是並行執行的,速度快,它也能夠配置成串行,在調試時比較有用,jest每一個測試用例文件都是一個沙箱,在單個測試文件內部定義或修改全局變量,不會影響其它測試文件,jest由Facebook團隊維護,對React友好,適合大型項目

mocha是一個精簡而靈活的單元測試框架,它自己沒有包含斷言庫和mock(模擬)功能,須要自行引入其它庫,而jest和jasmine都自帶斷言庫和mock功能,什麼是mock,後面會介紹

puppeteer是個神器,它並不只僅能夠作自動化集成測試,它自己是個node庫,自帶chromuium瀏覽器(因此npm安裝它比較慢),它提供了一些高級API經過DevTools協議控制headless chrome或chromuium,它也能夠配置爲使用有界面版的chrome,既然是瀏覽器,chrome能作到的它基本都能作到,chrome作不到的,它也能作到,用puppeteer作集成測試,測試用例是真正在真實的瀏覽器上執行的,下面幾點都是它所擅長的

  • 生成頁面屏幕截圖或pdf
  • 自動提交表單,作UI測試、模擬鍵盤輸入、鼠標操做等
  • 建立一個最新的自動化測試環境,用最新的JavaScript和瀏覽器功能,直接在最新的chrome中作測試
  • 捕獲你網站的時間線跟蹤,以幫助診斷性能問題

casperJS是一個基於PhantomJS的庫,它封裝了PhantomJS的API使它更容易使用,PhantomJS內置了webkit的內核,測試用例並非跑在真正的瀏覽器上面

本文的重點是jest和puppeteer,下面是實例和API都是基於這二者


 

 

Setup 

若是在執行jest測試用例以前須要作一些配置,在用例執行完作一些清除操做,那你須要瞭解下面4個API

beforeEach(callback)
在每一個test用例執行前執行回調callback,在單個測試文件內,它對每一個test都有效,若是它放在describe內部,那麼它只對describe內部的test用例有效,上面講過,describe內部能夠有多個test
afterEach(callback)
在每一個test用例執行後執行回調callback,做用域同beforeEach
beforeAll(callback)
在全部test用例執行前執行回調callback
afterAll(callback)
在全部test用例執行後執行回調callback
 

斷言 

在編寫測試時,您常常須要檢查值是否符合某些條件。Expect就是幹這個的,它有不少 匹配方法,實例1中的

expect(sum(1,1)).toBe(2);

 意思就是斷言函數sum執行的結果等於2,其它匹配方法包括但不限於:

  • 判斷某個變量是否認義:.toBeDefined();
  • 比較某個值是否大於指定數字:.toBeGreaterThan(number);
  • 檢查對象length屬性是否等於指定值:.toHaveLength(number);
  • ……

 

mock定時器 

業務代碼中常常會用到定時器,包括setTimeout、setInterval,在作單元測試的時候,若是傻傻地等定時器一秒一秒走那就很浪費時間,你們都是一秒鐘幾十萬上下的人,哪怕幾秒鐘也不會浪費,jest的mock功能,能夠模擬定時器執行,有4個重要的API必須瞭解一下:

  1. jest.useFakeTimers() 聲明在當前測試文件中使用模擬定時器,聲明後,能夠直接用expect(setTimeout).toHaveBeenCalledTimes(1)判判定時器調用的次數
  2. jest.runAllTimers() 當即執行全部定時器 
  3. jest.runOnlyPendingTimers() 當即執行掛起的定時器
  4. jest.advanceTimersByTime(msToRun) 提早msToTun毫秒執行定時器

第1個API須要注意,僅僅聲明jest.useFakeTimers(),定時器回調的代碼並不會執行,第二、三、4個API都會真正執行定時器回調代碼;

jest.runOnlyPendingTimers()執行掛起的定時器是什麼意思?其實就是即將要執行的那一個定時器,下面這段代碼,會調用兩次setTimeout,第一次是jest.useFakeTimers()觸發的,第二次是jest.runOnlyPendingTimers()觸發的

function timeout() { setTimeout(() => { console.count('count'); timeout(); }, 10000); } jest.useFakeTimers(); test('useFakeTimers', () => { timeout(); jest.runOnlyPendingTimers(); expect(setTimeout).toHaveBeenCalledTimes(2); });

若是把上一段測試用例的jest.runOnlyPendingTimers()換成jest.runAllTimers()會進入死循環


 

 

mock函數

手動實現了一個forEach函數,要測試它是否按預期執行回調,這裏模擬了一個回調函數mockCallback,模擬函數的好處是能夠獲取每次調用它的參數和它的執行次數,在項目中能夠模擬請求返回指定數據而無需訪問服務器

function forEach(items, callback) { for (let index = 0; index < items.length; index++) { callback(items[index]); } } test('test forEach', () => { const mockCallback = jest.fn(); forEach([0, 1], mockCallback); // The mock function is called twice
    expect(mockCallback.mock.calls.length).toBe(2); // The first argument of the first call to the function was 0
    expect(mockCallback.mock.calls[0][0]).toBe(0); // The first argument of the second call to the function was 1
    expect(mockCallback.mock.calls[1][0]).toBe(1); }); 

 

 

異步 

測試腳本中可能包含異步操做,若是不用異步方式寫test,test執行到最後一行就認爲測試完成,極可能測試失敗

方式一:done回調,傳入參數done,異步操做執行完後執行done()

// done
test('async test', done => { function callback(data) { expect(data).toBe('xx'); done(); } fetchData(callback); });

 

方式二:返回promise,test會等promise執行完才跳出
// return promise
test('async test', () => { //判斷當前測試有一個斷言被執行
    expect.assertions(1); return fetchData().then(data => { expect(data).toBe('xx'); }); });

 

方式三:.resolves/.rejects,一樣必須return promise

test('works with resolves', () => { expect.assertions(1); return expect(user.getUserName(5)).resolves.toEqual('xx'); });

 

方式四:ES8的async/await,能夠和.resolves/.rejects混合使用

// async/await can be used.
it('works with async/await', async () => { expect.assertions(1); const data = await user.getUserName(4); expect(data).toEqual('xx'); }); // async/await can also be used with `.resolves`.
it('works with async/await and resolves', async () => { expect.assertions(1); await expect(user.getUserName(5)).resolves.toEqual('xx'); });

 

關於describe還有兩個重要重要的方法應該瞭解下
describe.only(name, fn)

只執行該describe,其它describe會被忽略

describe.skip(name, fn)

 和.only相反,只跳過該describe,在調試時頗有用

 

puppeteer經常使用的幾個API也瞭解一下

  • puppeteer.launch() 實例化一個瀏覽器
  • browser.newPage(url) 打開新頁面
  • page.goto(url) 跳轉到url
  • page.$(selector) 選擇頁面元素,返回的是元素句柄(ElementHandle),不是真實DOM節點,selector底層實現用的就是document.querySelector
  • page.$$(selector) 同上,selector底層實現用的就是document.querySelectorAll,返回多個句柄
  • page.$eval(selector, pageFunction[, ...args]) 同上,返回的是pageFunction的返回值,在pageFunction內能夠獲取到真實DOM節點,如獲取元素ID,page.$eval('div', divs => divs.id);
  • page.$$eval(selector, pageFunction[, ...args]) 同上,selector底層實現用的就是document.querySelectorAll
  • page.click(selector[, options]) 點擊指定元素
  • page.type(selector, text[, options]) 改變元素的值,若是是react,會同時改變model層數據,就像真實用戶輸入 

 

 

單元測試 VS 集成測試 

兩種測試方法各有優缺點,具體用哪一種視項目具體狀況而定

相關文章
相關標籤/搜索