Skip to main content

[note] Jest 筆記

keywords: test, test-driven development, TDD#
npx jest --showConfig # 檢視設定
npx jest -- Checkbox # 測試檔名包含 Checkbox 的檔案

安裝與設定#

$ npm install jest --save-dev

package.json 中將 test 的指令改成:

// package.json
"scripts": {
"test": "jest" // --watch, --watchAll
},

預設情況下,Jest 會自動去找:

  • __tests__ 資料夾內的 .js, .jsx, .ts, .tsx
  • .test.spec 結尾的檔案,例如,Component.test.js or Component.spec.js

預設的情況下,jest 會去找專案資料夾中的 tests 資料夾:

$ mkdir __tests__

執行測試:

$ npm test -- --coverage # --coverage 會顯示覆蓋率

Jest Configuration#

Jest Configuration @ Offical Doc

在 Jest 中使用 Babel (Import)#

Using Babel @ Jest > Getting Started

預設的情況下,Jest 是在 Node 環境下執行,在 ESModule 還沒於 Node 環境普遍被支援前,可以使用 Babel。

安裝 babel 相關套件#

$ npm install babel-jest @babel/core @babel/preset-env -D

新增設定檔 babel.config.js#

// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};

檢視測試覆蓋率#

檢視測試覆蓋率有兩種方式,第一種是透過指令:

$ npm test -- --coverage # --coverage 會顯示覆蓋率

第二種是在 package.json 中設定:

// package.json
"scripts": {
"test": "jest"
},
"jest": {
"collectCoverage": true
},

或者:

// package.json
"scripts": {
"test": "jest --coverage"
},

基本樣式#

在 Jest 中,一個基本的測試會長這樣:

  • Test Suite:被 describe() 包起來的部分
  • Test Case:被 test() 包起來的部分
describe('here is test suite', () => {
test('here is test case', () => {
// here is assertions
});
});

Using Matcher#

Using Matcher @ Jest Docs > Introduction

常用的匹配#

toBe:比對值是否相同#

// toBe 使用 Object.is 來比對,若想要比對物件內容是否一樣需使用 toEqual
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});

toEqual:比對物件內容是否相同#

test('object assignment', () => {
const data = { one: 1 };
data['two'] = 2;
expect(data).toEqual({ one: 1, two: 2 });
});

not:是否不同#

test('adding positive numbers is not zero', () => {
expect(1 + 3).not.toBe(0);
});

資料型別檢驗#

keywords: expect.any(), expect.anyThing()#
expect.anything(); // matches anything but null or undefined
expect.any(Number); // matches anything that was created with the given constructor

匹配的時候記得要使用 expect.toEqual() 的方法,例如:

const number = 3;
expect(number).toEqual(expect.any(Number));

比對真偽值#

keywords: toBeNull, toBeUndefined, toBeDefined, toBeTruthy, toBeFalsy#
test('null', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
test('zero', () => {
const z = 0;
expect(z).not.toBeNull();
expect(z).toBeDefined();
expect(z).not.toBeUndefined();
expect(z).not.toBeTruthy();
expect(z).toBeFalsy();
});

比對數值#

keywords: toBeGreaterThan, toBeGreaterThanOrEqual, toBeLessThan, toBeLessThanOrEqual, toBeCloseTo#

對於浮點數(floating number)來說,應該使用 toBeCloseTo 而不要用 toBetoEqual,因為可能會有進位問題:

test('adding floating point numbers', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); This won't work because of rounding error
expect(value).toBeCloseTo(0.3); // This works.
});

比對物件中的屬性(object property)#

keywords: toMatchObject(object), objectContaining(), toEqual()#

What's the difference between '.toMatchObject' and 'objectContaining' @ stack overflow

要比對物件中的屬性可以使用 toMatchObject 或「 objectContaining() 搭配 toEqual()」 使用。一般來說,使用 toMatchObject 會是比較容易的做法

  • toMatchObject:匹配物件中的部分屬性和值即可
  • toEqual:物件中的屬性和值需要完全相同
  • objectContaining:較少用,可能會搭配 Array 的 toContaintoContainEqual 的方法使用

物件內不包含物件#

使用 toMatchObjectobject.containing() 有一樣的效果。只要 matched object 的屬性都有列在 expected object 內即通過

const schema = {
gender: expect.any(String),
age: expect.any(Number),
phone: expect.anything(),
interests: expect.any(Array),
};
test('object containing', () => {
const data = {
gender: 'female',
age: 30,
phone: '0987345672',
interests: ['computer', 'guitar'],
comments: 'Nothing to comment',
};
expect(data).toMatchObject(schema); // PASS
expect(data).toEqual(expect.objectContaining(schema)); // PASS
});

物件內包含物件#

當物件內又含有其他物件時,使用 toMatchObjectobject.containing() 的效果不同:

  • 通常用這個:使用 toMatchObject 中需包含所列出的屬性和值
  • 使用 objectContaining 的話,需包含該物件完整的屬性和值,除非物件內又使用 objectContaining 去做匹配
test('nested object', () => {
/**
* toMatchObject
*/
// 通過:物件內還有物件,包含完整的 props/values
expect({ position: { x: 0, y: 0 } }).toMatchObject({
position: {
x: expect.any(Number),
y: expect.any(Number),
},
});
// 主要差異處!!
// 通過:物件內還有物件,只包含部分的 props/values
expect({ position: { x: 0, y: 0 } }).toMatchObject({
position: {
x: expect.any(Number),
},
});
/**
* objectContaining
*/
// 通過:物件內還有物件,包含完整的 props/values
expect({ position: { x: 0, y: 0 } }).toEqual(
expect.objectContaining({
position: {
x: expect.any(Number),
y: expect.any(Number),
},
}),
);
// 主要差異處!!
// 失敗:物件內還有物件,但只包含部分的 props/values,而沒有再定義 expect.objectContaining
expect({ position: { x: 0, y: 0 } }).toEqual(
expect.objectContaining({
position: {
x: expect.any(Number),
},
}),
);
// 通過:物件內還有物件,但物件屬性內又定義 objectContaining
expect({ position: { x: 0, y: 0 } }).toEqual(
expect.objectContaining({
position: expect.objectContaining({
x: expect.any(Number),
}),
}),
);
});

匹配字串(透過正規式)#

keywords: toMatch#
test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in PJCHENder', () => {
expect('PJCHENder').toMatch(/stop/);
});

判斷回傳的是否為字串:

test('but there is a "stop" in PJCHENder', () => {
expect('PJCHENder').toEqual(expect.any(String));
});

匹配陣列是否包含#

keywords: toContain, toContainEqual#

判斷陣列中是否包含某一元素(原生值)時,可以使用 toContain

const shoppingList = ['diapers', 'kleenex', 'trash bags', 'paper towels', 'beer'];
test('the shopping list has beer on it', () => {
expect(shoppingList).toContain('beer');
expect(new Set(shoppingList)).toContain('beer');
});

判斷陣列中是否包含某一物件,可以使用 toContainEqual

const users = [
{ name: 'aaron', email: 'aaron@gmail.com' },
{ name: 'pjchender', email: 'pjchender@gmail.com' },
];
expect(users).toContainEqual({ name: 'aaron', email: 'aaron@gmail.com' });

如果是要判斷陣列中是否包含某一物件中的部分屬性,可以使用 toContainEqual 搭配 expect.objectContaining(),例如:

const users = [
{ name: 'aaron', email: 'aaron@gmail.com' },
{ name: 'pjchender', email: 'pjchender@gmail.com' },
];
// expect(users).toContainEqual({ name: 'aaron' }); // wrong: deep equality
expect(users).toContainEqual(
expect.objectContaining({
name: 'aaron',
}),
);

例外處理(Exception)#

keywords: toThrow#
function compileAndroidCode() {
throw new ConfigError('you are using the wrong JDK');
}
test('compiling android goes as expected', () => {
expect(compileAndroidCode).toThrow();
expect(compileAndroidCode).toThrow(ConfigError);
// You can also use the exact error message or a regexp
expect(compileAndroidCode).toThrow('you are using the wrong JDK');
expect(compileAndroidCode).toThrow(/JDK/);
});

Mock Functions#

Mock Functions @ Jest Docs > Introduction

基本使用#

keywords: jest.fn(<implementation>)#

若想要測試一個函式,透過 Mock Functions 可以檢驗該函式被呼叫了幾次、帶入的參數為何、回傳的結果為何等等。只需透過 jest.fn 即可建立 Mock Functions,並透過此函式的 mock 屬性即可看和此函式有關的訊息:

💡 jest.fn(implementation) 只是 jest.fn().mockImplementation(implementation) 的縮寫。

// 使用 jest.fn(<implementation>) 建立 Mock Function
it('mockCallback', () => {
// 建立 Mock Functions,取名為 mockCallback
const mockCallback = jest.fn((x) => 42 + x);
[0, 1].forEach(mockCallback);
// 檢查該函式被呼叫了幾次
expect(mockCallback.mock.calls.length).toBe(2);
// 該函式第一次被呼叫時的第一個參數為 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// 該函式第二次被呼叫時的第一個參數為 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// 該函式第一次被呼叫時的回傳值是 42
expect(mockCallback.mock.results[0].value).toBe(42);
});

Mock Function 預設會回傳 undefined

// jest.fn 預設回傳 undefined
it('default mock function', () => {
const myMock = jest.fn();
expect(myMock()).toBe(undefined);
});

設定回傳值(mock return values)#

keywords: mockReturnValueOnce, mockReturnValue#

透過 Mock Function 的 API 可以去修改 mock function 的回傳值,讓每一次呼叫得到不同的回傳值:

it('default mock function', () => {
const myMock = jest.fn();
expect(myMock()).toBe(undefined);
// 透過 mockReturnValueOnce 來設定該 Function 每次呼叫後會回傳的值
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
expect(myMock()).toBe(10);
expect(myMock()).toBe('x');
expect(myMock()).toBeTruthy();
});

模擬套件(mocking modules)#

keywords: jest.mock(<package>)#

假設我們使用 axios 這個套件,但我們不想要真的向 API 發送請求,而是想要模擬回傳的結果,可以使用 jest.mock(<package>) 來自動模擬 axios 套件。

// users.test.js
const fetch = require('node-fetch');
// 模擬 node-fetch 這個套件
jest.mock('node-fetch');
// 使用模擬後的 fetch 方法
it('mock modules and async', async () => {
const users = [{ name: 'Bob' }];
const fetchMock = fetch.mockResolvedValue(users);
const response = await fetchMock(); // 記得要執行 fetchMock() 才會有值
expect(response).toEqual(users);
});

模擬非同步的回傳結果#

// users.test.js
const fetch = require('node-fetch');
// 模擬 node-fetch 這個套件
jest.mock('node-fetch');
// 使用模擬後的 fetch 方法
it('mock modules and async', async () => {
const users = [{ name: 'Bob' }];
// 模擬後的 fetch 方法可以使用 mockResolvedValue 這個方法
// 產生一個會回傳 Promise 的函式
const fetchMock = fetch.mockResolvedValue(users);
const response = await fetchMock(); // 記得要執行 fetchMock() 才會有值
expect(response).toEqual(users);
});

模擬回呼函式(callback function with mock implementation)#

mock implementations @ Jest Docs

假設有一個函式中帶有 callback function:

// session.connection 這個方法帶有兩個參數
// 第一個是 token;第二個是 callback function
connect(token, (error) => {
if (error) {
handleError(error);
} else {
const connectionId = session.connection.connectionId;
action.update({
connectionId,
isSessionConnected: true,
});
}
});

撰寫測試時可以這樣透過 mockFn.mockImplementation() 的方式,例如:

// 第一個參數帶 token,第二個帶 callback function
// 其中執行時帶入的參數 null,就會變成實際上 callback function 中 error 的值
const connect = jest.fn((token, completeHandler) => completeHandler(null));

使用匹配與客製化匹配(Custom Matchers)#

方便易用的方法:

// 該 mock 函式至少被呼叫過一次
expect(mockFunc).toHaveBeenCalled();
// 該 mock 函式至少被呼叫一次,並且帶有特定的參數
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
// 該 mock 函式最後一次被呼叫時帶有特定的參數
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();

使用原生的 mock 物件做比對:

// 該 mock 函式至少被呼叫過一次
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
// 該 mock 函式至少被呼叫一次,並且帶有特定的參數
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);
// 該 mock 函式最後一次被呼叫時帶有特定的參數
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([arg1, arg2]);
// 該 mock 函式最後一次被呼叫時的第一個參數是 42
// (note that there is no sugar helper for this specific of an assertion)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
// A snapshot will check that a mock was invoked the same number of times,
// in the same order, with the same arguments. It will also assert on the name.
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe('a mock name');

測試非同步程式碼(Testing Asynchronous Code)#

Testing Asynchronous Code @ Jest Docs > Introduction

promise#

若使用的是 promise 可以直接將該 promise 在 test 的函式中 return 出來,Jest 會等待該 promise 被 resolve 後才繼續,若該 promise 被 reject,則該測試會自動失敗:

const fetchData = require('./../modules/fetchData');
const schema = {
userId: expect.any(Number),
id: expect.any(Number),
title: expect.any(String),
completed: expect.any(Boolean),
};
test('fetch data with json placeholder', () => {
return fetchData('https://jsonplaceholder.typicode.com/todos/1').then((data) => {
expect(data).toMatchObject(schema);
});
});

async...await#

const fetchData = require('./../modules/fetchData');
const schema = {
userId: expect.any(Number),
id: expect.any(Number),
title: expect.any(String),
completed: expect.any(Boolean),
};
test('fetch data with json placeholder', async () => {
expect.assertions(1);
try {
const data = await fetchData('https://jsonplaceholder.typicode.com/todos/1');
expect(data).toMatchObject(schema);
} catch (e) {
expect(e).toMatch('error');
}
});

callbacks#

如果測試的程式碼中有非同步的操作,可以在 test 的函式中帶入參數 done,如此 jest 會等到 done() 被執行時才結束測試;若 done() 一直沒有被呼叫到,則顯示測試失敗。

test('fetch data with json placeholder', (done) => {
fetchData('https://jsonplaceholder.typicode.com/todos/1').then((data) => {
expect(data).toMatchObject(schema);
done();
});
});

重複某一行為(Setup and Teardown)#

keywords: beforeEach, beforeAll, afterEach, afterAll#

setup and teardown @ Jest

開始測試前,常會有一些前置的操作或設定要先被執行,這裡 Jest 提供一些 helpers 來方便使用。

每個測試前都要重複執行(repeating setup for many tests)#

如果你有一些前置動作是要在許多測試前都要重複執行的,那麼可以使用 beforeEachafterEach

// 在每一個 test 前都會先執行 beforeEach,test 後都會執行 afterEach
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});

測試前執行一次(One-Time Setup)#

有些時候,這些設定只需要在開始前後(所有 test 執行前後)執行一次就好,這時候可以使用 beforeAllafterAll 的方法:

// 在所有 test 執行前先執行 beforeAll;所有 test 執行完後再執行 afterAll
beforeAll(() => {
return initializeCityDatabase();
});
afterAll(() => {
return clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});

Scope#

預設的情況下,beforeafter套用到一個檔案中的所有測試(test),透過 describe 你可以把這些測試分成不同區塊,在 describe 區塊中使用的 beforeafter 只會在該區塊內有作用。

// beforeAll, afterAll 會在該檔案中的所有 test 前後執行
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
// beforeEach, afterEach 會在該檔案中的每一個 test 前後執行
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
// 這裡的 before, after 只會作用在此 block 內
beforeAll(() => console.log('2 - beforeAll'));
afterAll(() => console.log('2 - afterAll'));
beforeEach(() => console.log('2 - beforeEach'));
afterEach(() => console.log('2 - afterEach'));
test('', () => console.log('2 - test'));
});
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll

測試事件(Event and EventEmitter)#

若要測試事件,可以使用 Node.js 中提供的 Event 和 EventEmitter

例如要測試第三方套件的 session 物件,該物件有 on, offdispatch 事件:

// session.js
// 建立第三方套件 session 的 mock modules
const EventEmitter = require('events');
const sessionEvent = new EventEmitter();
const session = {
dispatch: jest.fn((type) => {
sessionEvent.emit(type);
}),
on: jest.fn((type, callback) => {
sessionEvent.on(type, callback);
}),
off: jest.fn((type, callback) => {
sessionEvent.off(type, callback);
}),
};
module.exports = session;

測試時:

// session.test.js
const sessionEvent = require('./session');
it('test sessionEvent', () => {
const eventHandler = jest.fn();
sessionEvent.on('eventName', eventHandler);
expect(eventHandler.mock.calls.length).toBe(0);
sessionEvent.dispatch('eventName');
expect(eventHandler.mock.calls.length).toBe(1);
});

範例程式碼#

describe('Filter function', () => {
test('it should filter by a search term(link)', () => {
// actual test
const input = [
{ id: 1, url: 'https://www.url1.dev' },
{ id: 2, url: 'https://www.url2.dev' },
{ id: 3, url: 'https://www.link3.dev' },
];
const output = [{ id: 3, url: 'https://www.link3.dev' }];
expect(filterByTerm(input, 'LINK')).toEqual(output);
});
});
function filterByTerm(inputArr, searchTerm) {
return inputArr.filter((item) => item.url.match(searchTerm));
}

常見問題#

ReferenceError: document is not defined#

ReferenceError: document is not defined

可以透過 npx jest --showConfig 檢查 testEnvironment 是不是 jsdom,如果不是的話可以在 config 檔中加上:

// jest.config.js
module.exports = {
+ testEnvironment: 'jsdom',
};

或者透過 CLI 指定:

npx jest --env=jsdom

API Reference#

Mock Functions#

Mock Functions @ Jest API

Jest Object#

The Jest Object @ Jest API

Mock Modules#

Mock Functions#

Mock Timers#

Misc#

參考資源#

Last updated on