1. 程式人生 > >前端單元測試之Jest

前端單元測試之Jest

概述

關於前端單元測試的好處自不必說,基礎的介紹和知識可以參考之前的部落格連結:React Native單元測試。在軟體的測試領域,測試主要分為:單元測試、整合測試和功能測試。

  • 單元測試:在計算機程式設計中,單元測試(英語:Unit Testing)又稱為模組測試, 是針對程式模組(軟體設計的最小單位)來進行正確性檢驗的測試工作。程式單元是應用的最小可測試部件。在過程化程式設計中,一個單元就是單個程式、函式、過程等;對於面向物件程式設計,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。
  • 整合測試,也叫組裝測試或聯合測試。在單元測試的基礎上,將所有模組按照設計要求(如根據結構圖)組裝成為子系統或系統,進行整合測試。
  • 功能測試,就是對產品的各功能進行驗證,根據功能測試用例,逐項測試,檢查產品是否達到使用者要求的功能。

前端的測試框架有很多:mocha, jasmine, ava, testcafe, jest,他們都有各自擅長的領域和特點,而我們採用的jest框架具有如下的一些特點:

  • 適應性:Jest是模組化、可擴充套件和可配置的;
  • 沙箱和快速:Jest虛擬化了JavaScript的環境,能模擬瀏覽器,並且並行執行;
  • 快照測試:Jest能夠對React 樹進行快照或別的序列化數值快速編寫測試,提供快速更新的使用者體驗;
  • 支援非同步程式碼測試:支援promises和async/await;
  • 自動生成靜態分析結果:不僅顯示測試用例執行結果,也顯示語句、分支、函式等覆蓋率。

安裝

# yarn
yarn add --dev jest

# npm
npm install --save-dev jest
複製程式碼

我們編寫一個被測試檔案的sum.js,程式碼如下:

function sum(a, b) {
  return a + b;
}
module.exports = sum;
複製程式碼

然後,我們新增一個名為sum.test.js的測試檔案,注意命名時遵循xxx.test.js的命名規則。

const sum = require(‘./sum');
test('
adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3); }); 複製程式碼

內建斷言庫

“斷言”通常是給程式開發人員自己使用,並且在開發測試期間使用,用於判斷在某些邏輯條件下會執行某種預期的結果。Jest框架內建了豐富的斷言語句,詳細的可以參考Jest 的Expect。此處,列舉一些常用的:

.toBe(value)
.toHaveBeenCalled()
.toBeFalsy()
.toEqual(value)
.toBeGreaterThan(number)
.toBeGreaterThanOrEqual(number)
複製程式碼

舉個例子,下面是一個被測試的檔案Hook.js。

export default class Hook {

    constructor() {
        this.init();
    }

    init() {
        this.a = 1;
        this.b = 1;
    }

    sum() {
        return this.a  + this.b;
    }
}
複製程式碼

Hook.js主要實現兩個數字相加的功能,然後我們編寫一個測試檔案Hook.test.js。

import Hook from '../src/hook';

describe('hook', () => {
    const hook = new Hook;
    // 每個測試用例執行前都會還原資料,所以下面兩個測試可以通過。
    beforeEach( () => {
        hook.init();
    })
    test('test hook 1', () => {
        hook.a = 2;
        hook.b = 2;
        expect(hook.sum()).toBe(4);
    })
    test('test hook 2', () => {
        expect(hook.sum()).toBe(2);// 測試通過
    })
})
複製程式碼

然後,在控制檯執行yarn jest命令,即可執行單元測試,執行完成後會給出相應的結果。例如:

在這裡插入圖片描述

生命週期勾子

jest 測試提供了一些測試的生命週期 API,可以輔助我們在每個 case 的開始和結束做一些處理。 這樣,在進行一些和資料相關的測試時,可以在測試前準備一些資料,在測試完成後清理測試資料。這部分的知識可以參考官方的全域性API

這裡列舉4個主要的生命週期勾子:

  • afterAll(fn, timeout): 當前檔案中的所有測試執行完成後執行 fn, 如果 fn 是 promise,jest 會等待timeout 毫秒,預設 5000;
  • afterEach(fn, timeout): 每個 test 執行完後執行 fn,timeout 含義同上;
  • beforeAll(fn, timeout): 同 afterAll,不同之處在於在所有測試開始前執行;
  • beforeEach(fn, timeout): 同 afterEach,不同之處在於在每個測試開始前執行;
BeforeAll(() => {
  console.log('before all tests to excute !')
})

BeforeEach(() => {
  console.log('before each test !')
})

AfterAll(() => {
  console.log('after all tests to excute !')
})

AfterEach(() => {
  console.log('after each test !')
})

Test('test lifecycle 01', () => {
  expect(1 + 2).toBe(3)
})

Test('test lifecycle 03', () => {
  expect(2 + 2).toBe(4)
})
複製程式碼

mock

mock測試就是在測試過程中,對於某些不容易構造或者不容易獲取的物件,用一個虛擬的物件來建立以便繼續進行測試的測試方法。Mock函式通常會提供以下三種特性:

  • 捕獲函式呼叫情況;
  • 設定函式返回值;
  • 改變函式的內部實現;

jest.fn()

jest.fn()是建立Mock函式最簡單的方式,如果沒有定義函式內部的實現,jest.fn()會返回undefined作為返回值。例如:有兩個被測試程式碼every.js和foreach.js。程式碼如下: every.js

function every(array, predicate) {
  let index = -1
  const length = array == null ? 0 : array.length
  while (++index < length) {
    if (!predicate(array[index], index, array)) {
      return false
    }
  }
  return true
}
export default every
複製程式碼

foreach.js

function foreach(arr, fn) {
    for(let i = 0, len = arr.length;  i < len; i++) {
        fn(arr[i]);
    }
}

module.exports = foreach;
複製程式碼

下面是測試用例mock.test.js檔案的程式碼:

import foreach from '../foreach';
import every from '../every';

describe('mock test', () => {
    it('test foreach use mock', () => {
        // 通過jest.fn()生成一個mock函式
        const fn = jest.fn();
        foreach([1, 2, 3], fn);
        //測試mock函式被呼叫了3次
        expect(fn.mock.calls.length).toBe(3);
        // 測試第二次呼叫的函式第一個引數是3
        expect(fn.mock.calls[2][0]).toBe(3);
    })

    it('test every use mock return value', () => {
        const fn = jest.fn();
        fn
            .mockReturnValueOnce(true)
            .mockReturnValueOnce(false);

        const res = every([1, 2, 3, 4], fn);
        expect(fn.mock.calls.length).toBe(2);
        expect(fn.mock.calls[1][1]).toBe(1);
    })

    it('test every use mock mockImplementationOnce', () => {
        const fn = jest.fn((val, index) => {
            if (index == 2) {
                return false;
            }
            return true;
        });

        const res = every([1, 2, 3, 4], fn);
        expect(fn.mock.calls.length).toBe(3);
        expect(fn.mock.calls[1][1]).toBe(1);
    })
    
})
複製程式碼

手動mock

測試程式碼時可以忽略模組的依存關係,進行手動mock。例如,有一個測試檔案sum2.js。

function sum2(a, b) {
    if (a > 10) return a * b;
    return a + b;
}

export default sum2;
複製程式碼

如果要mock 一個sum2.js 檔案的話,需要在sum2.js 同級目錄下新建資料夾__mock__,然後在此檔案下新建檔案同名 sum2.js,然後mock返回100。

export default function sum2(a, b) {
    return 100;
}
複製程式碼

然後,新建一個mock_file.test.js測試檔案。

jest.mock('../sum2');
import sum2 from '../__mock__/sum2';

it('test mock sum2', () => {
    //因為此時訪問的是__mock__資料夾下的sum2.js所以測試通過
    expect(sum2(1, 11111)).toBe(100);
})
複製程式碼

非同步測試

在實際開發過程中,經常會遇到一些非同步的JavaScript程式碼。當有非同步方式執行的程式碼的時候,Jest需要知道當前它測試的程式碼是否已經完成,然後它才可以轉移動另一個測試中,也就是說,測試的用例一定要在測試物件結束之後才能夠執行。Jest的非同步測試主要分為3種:

  • done函式
  • return promise
  • async/await

done的例子如下:

function fetchData(call) {
  setTimeout(() => {
    call('peanut butter1')
  },1000);
}

test('the data is peanut butter', (done) => {
  function callback(data) {
    expect(data).toBe('peanut butter');
    done()
  }
  fetchData(callback);
});

複製程式碼

因為superagent庫支援 promise和async/await方式,所以用superagent舉例,實際專案開發可能會涉及到promise(es6以前的寫法)和async/await(最新的寫法),大家可以根據實際情況編寫測試程式碼。

import superagent from 'superagent';


const target = 'http://www.baidu.com';

describe('test promise async', () => {

    it('test done', done => {
        superagent.get(target).end((err, res) => {
            expect(res).toBeTruthy();
            done();
        });
    });

    it('test promise', () => {
        return superagent.get(target).then((res) => {
            expect(res).toBeTruthy();
        });
    });

    it('test async/await', async () => {
        const res = await superagent.get(target);
        expect(res).toBeTruthy();
    });
});
複製程式碼

注意,使用superagent框架進行非同步測試時,請確保你的專案安裝了superagent依賴。

Snapshot

快照測試第一次執行的時候會將被測試ui元件在不同情況下的渲染結果儲存一份快照檔案,後面每次再執行快照測試時,都會和第一次的比較,除非執行“yarn test – -u”命令刪除快照檔案。例如,有一個檔案reactComp.js.

import React from 'react';

export default class reactComp extends React.Component {
    render() {
        return (
            <div>我是react元件 </div>
        )
    }
}
複製程式碼

然後,編寫一個測試用例檔案reactComp.test.js。

import React from 'react';
import renderer from 'react-test-renderer';

import RC from '../reactComp';

test('react-comp snapshot test', () => {
    const component = renderer.create(<RC />);
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();
})

test('react-comp snapshot test2', () => {
    const component = renderer.create(<RC />);

    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();
})
複製程式碼

執行測試命令,會在test目錄下生成一個__snapshots__目錄,在此目錄下會與一個快照檔案,格式如下:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`react-comp snapshot test 1`] = `
<div>
  我是react元件
</div>
`;

exports[`react-comp snapshot test2 1`] = `
<div>
  我是react元件
</div>
`;

複製程式碼

如果被測試程式碼有正常更新,可以使用“jest --updateSnapshot ”命令重新更新快取檔案。

附:React Native單元測試 Jest測試官方文件