Jest學習筆記

一.是什麼?

Jest是Facebook開源的一個前端測試框架,自動集成了斷言、JSdom、覆蓋率報告等,內部也是使用了jasmine作爲基礎,在其上進行了封裝。主要用於React和React Native的單元測試,已被集成在create-react-app中。

二.特點

  • 速度快:支持並行執行,Jest 爲每一個單元測試文件創造一個獨立的運行環境。Jest 可以啓動多個進程同時運行不同的文件,充分利用多核CPU。單進程 100 秒才完成的測試執行過程,8 核只需要 12.5 秒。並且可以通過test.only這種語法只運行特定的測試用例。
  • 易配置:雖然號稱0配置,但在真實的項目中,還是要根據具體需求,進行配置。
  • IDE整合:配合vscode中的jest插件,編寫完測試用例後,IDE自動運行測試用例。
  • Snapshot:Jest能夠對React組件樹進行序列化,生成對應的字符串快照,通過比較字符串提供高性能的UI檢測。
  • 多項目並行:可以同時跑React和node的測試用例。
  • 覆蓋率:內置Istanbul,測試代碼覆蓋率,並生成對應的報告
  • Mock系統:Jest實現了一個強大的Mock系統,支持自動和手動mock。

三.實現原理

通過對比預期實際結果進行測試。
假如我們有一個這樣的函數需要測試,如下:

function add(a, b) {
    return a + b
}

是的,只是計算傳入參數之和,並返回。

測試代碼如下(模擬Jest內部實現)

function expect(result) {
    return {
        toBe: function(actual) {
            if(result !== actual) {
                throw new Error(`預期值和實際值不相等 預期${actual} 實際結果卻是${result}`)
            }
        }
    }
}

function test(desc, fn) {
    try {
        fn();
        console.log(`${desc} 通過測試`)
    }catch(e) {
        console.log(`${desc} 沒有通過測試 ${e}`)
    }
}

test("測試加法 1 + 1 是否等於 2", () => {
    expect(add(1, 1)).toBe(2);
})

四.生命週期函數

  • beforeAll(fn):所有測試用例執行之前執行的方法。
  • beforeEach(fn):每個測試用例執行之前執行的方法。
  • afterEach(fn):每個測試用例執行之後執行的方法。
  • afterAll(fn):所有測試用例執行之後執行的方法。
    全局和describe都可以有這四個生命週期函數。
    一個demo說明他們的執行順序:
export default class Counter {
    constructor() {
        this.number = 0;
    }
    addOne() {
        this.number += 1;
    }
    minusOne() {
        this.number -= 1;
    }
}
// 測試代碼
import Counter from './Counter';

describe("測試 Counter", () => {
    let counter = null;

    console.log("第一層describe");

    beforeAll(() => {
        console.log("beforeAll");
    })

    beforeEach(() => {
        console.log("beforeEach");
        counter = new Counter();
    })

    afterEach(() => {
        console.log("afterEach");
    })

    afterAll(() => {
        console.log("afterAll");
    })

    describe("測試增加相關的代碼", () => {

        console.log("第二層describe");

        beforeAll(() => {
            console.log("增加相關 -- beforeAll");
        })

        beforeEach(() => {
            console.log("增加相關 -- beforeEach");
        })

        afterEach(() => {
            console.log("增加相關 -- afterEach");
        })

        afterAll(() => {
            console.log("增加相關 -- afterAll");
        })

        test("測試 Counter 中的 addOne 方法", () => {
            console.log("測試 Counter 中的 addOne 方法");
            counter.addOne();
            expect(counter.number).toBe(1);
        })
    })
})

輸出結果:
在這裏插入圖片描述
執行說明:外層的before函數優先級最高,並且All優先於Each。而after則是內層的優先級最高,並且Each優先於All。

請注意:我在每個describe的最開始,都打印了一句。他們總是被最先打印出來。這說明,只要沒在生命週期函數中,每個describe塊中的其餘代碼總是被優先執行的。

生命週期函數有什麼用哩?
可以做一些初始化工作,我在上面的beforeEach中實例化了一個counter,這樣每個測試用例之間的counter就不會相互影響。

五.異步代碼的測試

  1. 回調函數類型
import axios from "axios";

export const c = (fn) => {
   axios.get("http://www.dell-lee.com/react/api/demo.json").then(res => {
       fn(res.data);
   })
}

向某一接口發起請求,請求成功後,用返回的數據作爲參數調用回調函數。

test("fecthData1 返回結果爲 { success: true }", (done) => {
   fetchData1((data) => {
       expect(data).toEqual({
           success: true
       })
       done();
   })
})

這種形式的異步函數測試,一定要加done參數,因爲fetchData1函數時異步的,如果不加,回調函數還沒有被調用,test函數就執行完成了,測試就會直接通過了。所以,通過done參數這種形式,只有done函數被調用了,纔會認爲test執行完成。

  1. Promise類型
export const fetchData2 = () => {
    return axios.get("http://www.dell-lee.com/react/api/demo.json")
}
test("fecthData2 返回結果爲 { success: true }", () => {
    return fetchData2().then(res => {
        expect(res.data).toEqual({
            success: true
        })
    })
})

測試這種返回Promise類型時,一定要在前面加上return。這樣才能讓jest明白這是個異步過程,纔會去等待異步執行結果。

  1. async和await類型
    還是上面的那個fetchData2函數
test("fecthData2 返回結果爲 { success: true }", async () => {
    const res = await fetchData2();
    expect(res.data).toEqual({
        success: true
    })
})

個人認爲,這種測試代碼最好理解,比較推薦使用這種形式。

最後,有時候,我們就是想去測試404。該怎麼做哩?

export const fetchData3 = () => {
    return axios.get("http://www.dell-lee.com/react/api/demo1.json")
}

上面這個接口不存在,所以會返回404。

test("fecthData3 返回結果爲 404", () => {
    expect.assertions(1);
    return fetchData3().catch(e => {
        expect(e.toString().indexOf("404") > -1).toBe(true);
    })
})

使用expect.assertions(1),必須要一個斷言被執行,否則測試失敗。這樣當請求接口出錯時,就會執行catch中的代碼,從而保證有一個expect被執行了。

六.定時器的測試

export default (callback) => {
	setTimeout(() => {
		callback();
	}, 3000)
}
import timer from './timer';
test('定時器測試', (done) => {
	timer(() => {
		expect(1).toBe(1);
		done();
	})
})

這是一種最簡單的定時器測試代碼,但存在很大的問題。如果設置的定時時間是幾百秒哩?難道我們要等待幾百秒嗎?
如何解這個問題哩?

import timer from './timer';

jest.useFakeTimers();
test("timer 測試", () => {
    const fn = jest.fn();
    // 這樣timer還是會等待定時器設置的時長再執行
    timer(fn);
    // 所以必須配合下面的代碼
    // 這種代碼的意思是讓timer立刻執行,我們就不必等待了
    jest.runAllTimers();
    expect(fn).toHaveBeenCalledTimes(1);
})

有時候,我們會有這種需求,定時器的嵌套。

export default (callback) => {
    setTimeout(() => {
        callback();
        setTimeout(() => {
            callback();
        }, 2000)
    }, 1000)
}

我們只想測試第一層的timer,如果使用runAllTimers,內層的timer也會直接運行。如何解決?

import timer from './timer';

jest.useFakeTimers();
test("timer 測試", () => {
    const fn = jest.fn();
    // 這樣timer還是會等待定時器設置的時長再執行
    timer(fn); 
    // 所以必須配合下面的代碼
    // 這種代碼的意思是讓timer立刻執行,我們就不必等待了,
    // 只運行處於隊列中的timer,沒有被添加到隊列中的timer不會被執行
    jest.runOnlyPendingTimers();
    expect(fn).toHaveBeenCalledTimes(1);
})

上面這種方式理解難度較大,所以可以使用時間快進的函數。

import timer from './timer';

jest.useFakeTimers();
test("timer 測試", () => {
    const fn = jest.fn();
    timer(fn); 
    // advanceTimersByTime 快進時間
    jest.advanceTimersByTime(1000);
    expect(fn).toHaveBeenCalledTimes(1);
    // 時間快進是在上一次的基礎上,進行的
    // 所以說,這個測試用例的時間快進可能會影響其餘的測試用例, 
    // 所以可以在鉤子函數中執行 jest.useFakeTimers() 從而初始化定時器
    jest.advanceTimersByTime(2000);
    expect(fn).toHaveBeenCalledTimes(2);
})

七.Mock

  1. mock函數,捕獲函數的調用和返回結果,以及this指向,和調用順序。
import axios from 'axios';

export const runCallback = (callback) => {
    callback();
} 
test("測試 runCallback", () => {
    // 只要能證明func被調用過,那麼runCallback函數就被正確的調用啦。
    const func = jset.fn();
    runCallback(func);
    expect(func).toBeCalled();
})
  1. mock函數,自由的設置返回結果
test("測試 runCallback", () => {
    const func = jest.fn(() => {
        return "456"
    });
    
    func.mockReturnValueOnce("afeng");
    
    /* func.mockImplementation(() => {
        console.log(1);
        return "yafeng"
    }) */
    
    runCallback(func);
    runCallback(func);
    console.log(func.mock); 
    expect(func.mock.calls.length).toBe(2);
})

上面測試中,我們打印了func.mock,其上存在以下屬性。
在這裏插入圖片描述
calls:函數的調用次數,以及每次調用傳入的參數。
instances:函數調用時,this的指向。
invocationCallOrder:函數調用的順序,
results:函數調用的返回結果。

  1. mock函數,可以改變函數的內部實現。
export const getData = () => {
   return axios.get('/api').then(res => res.data)
}

假如我們有以上異步代碼需要測試,在真實的項目中,我們不會真正的去發送這種ajax請求。假設我們項目中有1000個這樣的異步請求,每一個請求的時間是3s,那麼我們運行一次測試,需要耗費大量的時間。所以在前端測試這一塊,我們只需要確認請求發送出去了就可以了,至於,後端接口是否可以正常返回,這是後端測試需要做的。

import axios from 'axios';
// mock axios,不會發送真實的請求,會使用模擬的數據
jest.mock("axios");

test("測試 getData", async () => {
    // 3. mock函數,改變函數的內部實現,本來是要異步的發請求,現在改爲同步的返回數據
    axios.get.mockResolvedValue({data: "hello"});
    await getData().then((data) => {
        expect(data).toBe("hello")
    })
})

上面,我們是通過mock,axios的get請求來進行同步的準備數據的。其實我們也可以mock,fetchData函數。如何做呢?

在項目的根目錄下新建__mocks__文件夾,建一個與需要mock的同名的js文件。
比如我們需要mock下面這個fetchData函數的實現。

import axios from 'axios';

export const fetchData = () => {
    return axios.get("/").then(res => res.data);
}

export const getNumber = () => {
    return "456"
}

在__mocks__中新建一個js文件。代碼如下:

export const fetchData = () => {
    return new Promise((resolved, reject) => {
        resolved(
            "(function() {return '123'})()"
        )
    })
}

測試代碼如下:

jest.mock('./demo');

import { fetchData } from './demo';
const { getNumber } = jest.requireActual('./demo');

test("測試fetchData", () => {
    return fetchData().then(data => {
        expect(eval(data)).toBe("123");
    })
})

test("測試fetchData", () => {
    expect(getNumber()).toBe("456")
})

由於我們使用了jest.mock();所以當import時,引用的將是mock的fetchData函數。

八.類的測試

我們有這樣的一個類

export default class Util {
    a() {
	// 邏輯異常複雜
    }

    b() {
	// 邏輯異常複雜
    }
}

關於類的測試,我們會有一個專門的測試文件,現在的需求是這樣的,我們有一個模塊,導入了這個類,並且調用了a,b兩個方法。
所以,當我們測試這個模塊時,我們不想真正的調用這兩個方法。因此可以使用Mock。

import Util from './util'; 

const demoFunction = (a, b) => {
    const util = new Util;
    util.a(a);
    util.b(b);
}

export default demoFunction;
jest.mock("./util.js");
// jest.mock 發現 util 是一個類,會自動的把類的構造函數和方法變成 jest.fn()
// 背後原理如下:
// const Util = jest.fn();
// Util.a = jest.fn();
// Util.b = jest.fn();
// 由於類的方法都是一個 jest.fn() 所以我們可以對類的方法執行做追溯
import demoFunction from './demo';
import Util from './util';

test("測試 demoFunction", () => {
    // 要想測試demoFunction成功執行了,只需驗證他調用了Util類創建實例,並且調用a,b 方法.
    // 但由於Util中的方法都異常複雜,全部執行,很耗性能。所以我們需要去模擬Util類
    demoFunction();
    expect(Util).toHaveBeenCalled();
    expect(Util.mock.instances[0].a).toHaveBeenCalled();
    expect(Util.mock.instances[0].b).toHaveBeenCalled();
})
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章