1. 環(huán)境準(zhǔn)備
初始項(xiàng)目
cnpm init
//或者
yarn init
安裝jest
cnpm install --save-dev jest
// 或者
yarn add --dev jest
下載和配置babel
(如果不使用ES6語(yǔ)法,則可以跳過(guò)此步驟)
cnpm install @babel/plugin-transform-modules-commonjs --save-dev
新建一個(gè).babelrc文件,寫(xiě)入如下內(nèi)容,或者直接在package.json中配置。
{
"env": {
"test": {
"plugins": ["@babel/plugin-transform-modules-commonjs"]
}
}
}
2. 基礎(chǔ)函數(shù)測(cè)試
① toBe
精確的比較,本質(zhì)是用Object.is來(lái)比較,取反使用not.toBe
export function sum(a, b) {
return a + b;
}
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
② toEqual
可以用來(lái)比較對(duì)象的值是否相同
export function returnObj(age) {
return {
name: "小哈",
age: age + 1
}
}
test('compare object', () => {
let result = returnObj(10);
expect(result).toEqual({name: "小哈", age: 11 });
expect(result).toBe({name: "小哈", age: 11 });
});
③ 函數(shù)拋出異常的測(cè)試
// 3. 拋出異常
export function throwError() {
throw new Error("i am sorry,there has something wrong!")
}
test('test error funciton', () => {
expect(throwError).toThrow(Error);
})
④ 其他可以根據(jù)特定的情況選擇方法
test('sth special in need', () => {
let data1 = null, data2, data3 = 0.1 + 0.2;
expect(data1).toBeNull;
expect(data2).toBeDefined;
expect(data3).toBeCloseTo(0.3);
expect(1 === 1).toBeTruthy();
expect(0 === 1).toBeFalsy();
})
浮點(diǎn)數(shù)的比較,由于精度的差異,則可以使用toBeCloseTo來(lái)比較
⑤專門測(cè)試,跳過(guò)其他測(cè)試
用于重點(diǎn)排查一些錯(cuò)誤,可以使用only,例:
test.only('...', () => {
expect(true).toBe(false);
});
3. 異步函數(shù)測(cè)試
在js中最常見(jiàn)的就是請(qǐng)求異步函數(shù)了,業(yè)務(wù)中基本都是增刪改查,CURD;如果在測(cè)試中直接使用回調(diào)函數(shù),但是jest測(cè)試執(zhí)行到末尾就會(huì)結(jié)束,不會(huì)等待回調(diào)完成;
舉個(gè)例子:
異步函數(shù)定義
export function fetchData(callback) {
try {
setTimeout(() => {
callback("success");
}, 2000)
}catch(err) {
callback("failed");
}
}
①直接調(diào)用測(cè)試:
import { fetchData, Asynchronous } from './functionModules';
test("call the async function directly", () => {
function callback(info) {
expect(info).toBe("success");
console.log("I'm callback function, I have performed")
}
fetchData(callback);
})
可以發(fā)現(xiàn)打印沒(méi)有被執(zhí)行,直接測(cè)試成功,等不及callback執(zhí)行,直接跳過(guò)了。
②對(duì)①進(jìn)行改進(jìn),使用done
test("call the async function with done", done => {
function callback(info) {
try{
expect(info).toBe("success");
done();
}catch(err) {
done(err);
}
}
fetchData(callback);
})
jest測(cè)試會(huì)等待done被執(zhí)行,如果沒(méi)有任何done被執(zhí)行,則會(huì)發(fā)生超時(shí)錯(cuò)誤,所以我們要使用try...catch來(lái)捕獲異常,所以此處會(huì)等待expect被執(zhí)行。
③使用返回一個(gè)promise的方法
注意點(diǎn)就是一定要return!!!
調(diào)用的函數(shù):
/**
* 返回異步函數(shù)
*/
export function Asynchronous() {
return new Promise((resolve, reject) => {
try {
setTimeout(() => {
resolve("success")
}, 2000)
}catch(err) {
reject("failed")
}
})
}
/**
* 返回異步函數(shù),發(fā)生錯(cuò)誤情況reject
*/
export function AsynchronousFailed() {
return new Promise((resolve, reject) => {
try {
throw Error("failed");
}catch(err) {
reject("failed")
}
})
}
jest測(cè)試,以下都會(huì)通過(guò)
test("use promise when resolve", () => {
return Asynchronous().then(res => {
expect(res).toBe("success")
})
})
test("use promise when resolve", () => {
return AsynchronousFailed().catch(err => {
expect.assertions(1);
expect(err).toBe("failed")
})
})
可以發(fā)現(xiàn)此處用到了expect.assertions,顧名思義,此處就是聲明斷言的次數(shù),經(jīng)常用于異步的代碼中,為了確保斷言的回調(diào)能夠按照預(yù)期進(jìn)行。
expect.assertions
④直接測(cè)試resolve/reject
同樣的需要return返回才會(huì)等待異步的執(zhí)行,注意是resolve + s, reject + s
test("judge resolve directly", () => {
return expect(Asynchronous()).resolves.toBe("success")
})
test("judge reject directly", () => {
return expect(AsynchronousFailed()).rejects.toBe("failed")
})
如果使用async,await則可以改寫(xiě)成:
test("judge reject directly when use async", async () => {
await expect(AsynchronousFailed()).rejects.toBe("failed")
})
test("judge reject directly when use async", async () => {
await expect(AsynchronousFailed()).rejects.toBe("failed")
})
4. 生命周期
- 當(dāng)我們需要在每個(gè)單元測(cè)試前都執(zhí)行某些操作,我們可以使用
beforeEach,相應(yīng)的也有每次執(zhí)行結(jié)束之后的方法:afterEach; - 如果我們需要在每次測(cè)試的開(kāi)頭,有且進(jìn)執(zhí)行一次,則可以使用
beforeAll, 對(duì)應(yīng)結(jié)束之后的方法:afterAll。 - 如果是對(duì)特定的單元測(cè)試,特定時(shí)候執(zhí)行某些操作,則可以使用
describe塊包裹起來(lái),這樣在塊中定義的方法只有對(duì)內(nèi)部起作用。
舉個(gè)例子:
import { sum } from './functionModules';
beforeEach(() => console.log("beforeEach-1"));
afterEach(() => console.log("afterEach-1"));
beforeAll(() => console.log("beforeAll-1"));
afterAll(() => console.log("afterAll-1"));
test("test1", () => console.log("test1"));
console.log("outer")
describe("test2", () => {
beforeEach(() => console.log("beforeEach-2"));
afterEach(() => console.log("afterEach-2"));
beforeAll(() => console.log("beforeAll-2"));
afterAll(() => console.log("afterAll-2"));
test("child test", () => {
console.log("test2")
expect(sum(1, 2)).toBe(3)
})
console.log("inner")
})
test("test3", () => console.log("test3"));
優(yōu)先級(jí)規(guī)則:
beforeAll ->
beforeEach -> 測(cè)試內(nèi)容1 -> afterEach
beforeEach -> 測(cè)試內(nèi)容2 -> afterEach
// 如果有describe, 繼續(xù)按照此規(guī)則
afterAll
注意describe中的test執(zhí)行時(shí),全局的beforeEach也會(huì)被執(zhí)行,jest會(huì)在真正測(cè)試開(kāi)始之前執(zhí)行所有其他代碼,默認(rèn)按測(cè)試順序執(zhí)行,執(zhí)行結(jié)果如下:
outer
inner
beforeAll-1
// 執(zhí)行外層第一個(gè)test
beforeEach-1
test1
afterEach-1
// 執(zhí)行describe塊
beforeAll-2
beforeEach-1
beforeEach-2
test2
afterEach-2
afterEach-1
afterAll-2
// 執(zhí)行外層第二個(gè)test
beforeEach-1
test3
afterEach-1
afterAll-1
5. mock函數(shù)
我們可以擦除函數(shù)的實(shí)際實(shí)現(xiàn),使用mock函數(shù)
- 捕獲對(duì)函數(shù)的調(diào)用
- 改變內(nèi)部結(jié)構(gòu)
- 在使用
new實(shí)例化時(shí)捕獲構(gòu)造函數(shù)的實(shí)例 - 允許測(cè)試時(shí)配置返回值
下面介紹幾種mock函數(shù)中常用的方法和屬性:
① jest.fn()
使用mock函數(shù),我們可以根據(jù)特定的方法或者mock屬性獲取返回值,調(diào)用參數(shù),調(diào)用情況等等。
test("mock function", () =>{
let fn = jest.fn();
// 使用mockReturnValueOnce可以鏈?zhǔn)皆O(shè)置返回值
fn.mockReturnValueOnce('a').mockReturnValueOnce('b');
let res1 = fn(1, 2), res2 = fn(3, 4);
// 獲取返回值
expect(res1).toBe('a');
expect(res2).toBe('b');
expect(fn.mock.results[0].value).toBe('a');
expect(fn.mock.results[1].value).toBe('b');
// 是否被調(diào)用了
expect(fn).toBeCalled();
// 判斷調(diào)用的次數(shù)
expect(fn).toBeCalledTimes(2);
expect(fn.mock.calls.length).toBe(2);
// 判斷調(diào)用的參數(shù)
expect(fn).toHaveBeenCalledWith(1, 2);
expect(fn).toHaveBeenCalledWith(3, 4);
// 第一次調(diào)用的第一個(gè)參數(shù)值
expect(fn.mock.calls[0][0]).toBe(1);
}, [])
使用mock promise函數(shù)
test('test jest.fn() by Promise', async () => {
let fn = jest.fn().mockResolvedValue('a');
let res = await fn();
expect(res).toBe('a');
expect(Object.prototype.toString.call(fn())).toBe("[object Promise]");
})
測(cè)試axios請(qǐng)求的API:
/**
* axios 請(qǐng)求函數(shù)
* @param { function } callback
*/
export async function getToDoList(callback) {
return axios.get("https://jsonplaceholder.typicode.com/todos").then(res => {
callback(res.data)
return res;
})
}
對(duì)應(yīng)的jest測(cè)試:
test("test axios", async () => {
expect.assertions(1);
let fn = jest.fn();
await getToDoList(fn);
console.log(fn.mock.calls[0][0].data) // 打印出結(jié)果
expect(fn).toBeCalledTimes(1); // 判斷fn是否被調(diào)用了一次
})
還可以不調(diào)用具體的API,通過(guò)mock返回假數(shù)據(jù),即:fake response
import axios from 'axios';
import { getToDoList } from './functionModules';
jest.mock('axios');
test("pretend to call axios api", async () => {
let myResult = { data: [{id: 1, title: "get up early", complete: false}] };
axios.get.mockResolvedValue(myResult);
//axios.get.mockImplementation(() => Promise.resolve(myResult))
return getToDoList(() => {}).then(data => expect(data).toEqual(myResult));
})
注意此處要和上面的代碼分開(kāi)兩個(gè)文件,否則會(huì)影響真實(shí)API測(cè)試的結(jié)果。當(dāng)我們需要定義一個(gè)模塊函數(shù)的默認(rèn)實(shí)現(xiàn)時(shí),我們可以使用mockImplementation來(lái)模擬數(shù)據(jù),如上,使用注釋的語(yǔ)句,測(cè)試也能通過(guò)。
以下代碼來(lái)自官網(wǎng)
// foo.js
module.exports = function () {
// some implementation;
};
// test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
同樣可以根據(jù)調(diào)用順序設(shè)置返回值,當(dāng)mockImplementationOnce定義的都執(zhí)行完成了,則會(huì)執(zhí)行jest.fn中定義的。
const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));
myMockFn((err, val) => console.log(val));
// > true
myMockFn((err, val) => console.log(val));
// > false