深入淺出 JavaScript 軟體測試 — #3 Test Runner — Jest

C.T. Lin
7 min readNov 10, 2021

上一篇介紹了如何自行寫出能執行的測試,不過實際上大部分時候我們都不該這樣做,我們應該使用開源軟體,輕鬆地站在巨人的肩膀上。

後面的 JavaScript 相關測試與我個人的 Best Practices 都會搭配著 Jest 介紹,不過熟知各種不同的 runner 還是有其必要性,畢竟有時候專案用什麼你就得跟著去使用。JavaScript 比較常見的 runner 大概有一下幾種,稍微知道即可:

Jest — 我最推薦的 runner,可以從小專案用到大專案
Mocha — 作為非常老牌的 runner,還是有一定的出現機率
Tap — 非常輕量簡單的 runner,實作跨語言的 Test Anything Protocol

接下來要來教大家如何用 Jest 換掉原本自行執行的測試。

安裝 Jest

首先是要先把 Jest 安裝到 devDependencies 裡,看你是用 npm 還是 yarn

npm install -D jest// 如果你是用 yarn 安裝的話
yarn add -D jest

接著要來把在 package.json裡面的 test npm script 換掉,換成 jest 指令:

// package.json
{
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^27.3.1"
}
}

這樣就算是裝好 Jest 的測試環境了。

Jest 怎樣認定測試檔案

在目前這個版本,Jest 搜尋測試檔案是使用下面這兩個 glob pattern:

- **/__tests__/**/.[jt]s?x
- **?(*.)+(spec|test).[jt]s?(x)

也就是說在不修改設定的情況下,你必須把測試檔案放入 __tests__ 資料夾,或是命名為 spec.js 或是 test.js 這類的檔名。

我們在上一篇使用了 test.js 這個檔名,所以在這邊剛好是無縫接軌的,可以直接進入改寫測試檔的步驟。

用 Jest 改寫 assert 的測試

我們上一篇所寫的一些測試,把它們全部整理起來的話,大概會是這個樣子:

const assert = require('assert');const items = [1, 2, 3].map(number => `item_${number}`);assert(items[0] === 'item_1', '第 1 個 item 應該是 item1');
assert(items[1] === 'item_2', '第 2 個 item 應該是 item2');
assert(items[2] === 'item_3', '第 3 個 item 應該是 item3');
assert(' A'.trim() === 'A', '左邊能 trim 掉');
assert('A '.trim() === 'A', '右邊能 trim 掉');
assert(' A '.trim() === 'A', '兩邊都能 trim 掉');
assert(' A A '.trim() === 'A A', '中間不會被 trim 掉');
function doSomething(callback) { /** **/ }let fnBeCalledWithArgs;
const fn = (...args) => {
fnBeCalledWithArgs = args
};
doSomething(fn);assert(fnBeCalledWithArgs, 'callback 有被 call');
assert(fnBeCalledWithArgs[0] === 10000, '被 call 時參數是 10000');

其中大致包含了三項不同的測試,我們這邊就一個一個來改寫它們。

用 Jest 改寫 Array.prototype.map(...) 的測試

首先是關於 Array.prototype.map(...) 的測試:

const assert = require('assert');const items = [1, 2, 3].map(number => `item_${number}`);assert(items[0] === 'item_1', '第 1 個 item 應該是 item1');
assert(items[1] === 'item_2', '第 2 個 item 應該是 item2');
assert(items[2] === 'item_3', '第 3 個 item 應該是 item3');

我們需要把定義的測試用 it包起來,並給它一個適當的描述。再來,我們不需要引入 assert 了,取而代之的是 Jest global 有一個更好用的斷言(Assertion)語法 — expect 可以用,toEqual 這邊可以幫我們檢查兩個陣列是否相等( deep equality):

it('Array.prototype.map(...) 應該回傳包含映射結果的新陣列', () => {
const items = [1, 2, 3].map(number => `item_${number}`);
expect(items).toEqual(['item_1', 'item_2', 'item_3');
});

好的 assertion 可以讓測試可讀性變高,也可以避免在測試寫太多邏輯,可以得到更高的可靠性。可以在官網去看詳細的 expect API

用 Jest 改寫 String.prototype.trim() 的測試

接著來看 String.prototype.trim() 的測試:

const assert = require('assert');assert(' A'.trim() === 'A', '左邊能 trim 掉');
assert('A '.trim() === 'A', '右邊能 trim 掉');
assert(' A '.trim() === 'A', '兩邊都能 trim 掉');
assert(' A A '.trim() === 'A A', '中間不會被 trim 掉');

跟前面一樣,用 it 把描述寫清楚,裡面換成 Jest 的 expect

it('String.prototype.trim() 應該能把兩邊空白拿掉', () => {
expect(' A'.trim()).toBe('A');
expect('A '.trim()).toBe('A');
expect(' A '.trim()).toBe('A');
expect(' A A '.trim()).toBe('A A');
});

如果希望這個項目測試的更細緻一點的話,可以選擇把 case 分成更細一點:

it('String.prototype.trim() 應該能把左邊空白拿掉', () => {
expect(' A'.trim()).toBe('A');
});
it('String.prototype.trim() 應該能把右邊空白拿掉', () => {
expect('A '.trim()).toBe('A');
});
it('String.prototype.trim() 不會把中間空白 trim 掉', () => {
expect(' A A '.trim()).toBe('A A');
});

用 Jest 改寫 Side Effect 的測試

最後是改寫 Side Effect 的測試

const assert = require('assert');function doSomething(callback) { /** **/ }let fnBeCalledWithArgs;
const fn = (...args) => { fnBeCalledWithArgs = args };
doSomething(fn);assert(fnBeCalledWithArgs, 'callback 有被 call');
assert(fnBeCalledWithArgs[0] === 10000, '被 call 時參數是 10000');

這邊我們可以用 jest.fn() 來製造假 function 傳進去,就可以直接用 toBeCalledWith 的 assertion,非常方便:

function doSomething(callback) { /** **/ }it('doSomething 會用 10000 當參數呼叫 callback', () => {
const fn = jest.fn();
doSomething(fn); expect(fn).toBeCalledWith(10000);
});

這樣一來三個部分就改好啦。

結語

Jest 是彈性很高但實際上上手很簡單的 runner,希望這篇有讓初學的人都能體會到這一點。下一篇要來講講怎麼做 server side 的測試。

--

--

C.T. Lin

Architect @ Dcard. Author of Electron React Boilerplate and Bottender. JavaScript Developer.