Jest前端測試框架入門
近年來,隨著前端工程化的發展,前端發生了翻天覆地的變化。jQuery已經慢慢淡出了我們的視野,React、Vue和anglur三駕馬車急速駛來。從此,前端進入了資料驅動的時代,也有了清晰的模組化開發的方式。隨之而來的就是如何去保證自己的程式碼的正確性。
為什麼需要前端自動化測試
編寫測試程式碼要在正是寫程式碼前進行的,它就相當於具體明確的需求文件。之後我們寫的程式碼如果能通過測試程式碼就證明是符合預期的。
除此之外,由於一個專案需要多人維護,也許別人不小心改動了你的程式碼就會導致新的問題。所以提交程式碼前需要跑一遍測試用例,確保自己沒有改動別人的邏輯。如果有改動別人的程式碼,一定要弄清楚這樣改動會不會產生新的問題,最後記得把測試用例程式碼也要改下。
前端測試工具也和前端的框架一樣紛繁複雜,其中常見的測試工具,大致可分為測試框架、斷言庫、測試覆蓋率工具等幾類。在正式開始本文之前,我們先來大致瞭解下它們:
測試框架
測試框架的作用是提供一些方便的語法來描述測試用例,以及對用例進行分組。
測試框架可分為兩種: TDD (測試驅動開發)和 BDD (行為驅動開發),我理解兩者間的區別主要是一些語法上的不同,其中 BDD 提供了提供了可讀性更好的用例語法,至於詳細的區別可參見 The Difference Between TDD and BDD 一文。
常見的測試框架有 Jasmine, Mocha 以及本文要介紹的 Jest 。
斷言庫
斷言庫主要提供語義化方法,用於對參與測試的值做各種各樣的判斷。這些語義化方法會返回測試的結果,要麼成功、要麼失敗。常見的斷言庫有 Should.js, Chai.js 等。
測試覆蓋率工具
用於統計測試用例對程式碼的測試情況,生成相應的報表,比如 istanbul 。
Jest
為什麼選擇Jest?
Jest 是 Facebook 出品的一個測試框架,相對其他測試框架,其一大特點就是就是內建了常用的測試工具,比如自帶斷言、測試覆蓋率工具,實現了開箱即用。
而作為一個面向前端的測試框架, Jest 可以利用其特有的快照測試功能,通過比對 UI 程式碼生成的快照檔案,實現對 React 等常見框架的自動測試。
此外, Jest 的測試用例是並行執行的,而且只執行發生改變的檔案所對應的測試,提升了測試速度。目前在 Github 上其 star 數已經破兩萬;而除了 Facebook 外,業內其他公司也開始從其它測試框架轉向 Jest ,比如 Airbnb 的嘗試 ,相信未來 Jest 的發展趨勢仍會比較迅猛。
安裝
Jest 可以通過 npm 或 yarn 進行安裝。以 npm 為例,既可用npm install -g jest
進行全域性安裝;也可以只區域性安裝、並在 package.json 中指定 test 指令碼:
{
"scripts": {
"test": "jest"
}
}
Jest 的測試指令碼名形如*.test.js
,不論 Jest 是全域性執行還是通過npm run test
執行,它都會執行當前目錄下所有的*.test.js
或 *.spec.js
檔案、完成測試。
用法
具體用法參考JEST官網,我們這裡只是簡單介紹幾個常規用法。
用例的表示
表示測試用例是一個測試框架提供的最基本的 API , Jest 內部使用了 Jasmine 2 來進行測試,故其用例語法與 Jasmine 相同。test()
函式來描述一個測試用例,舉個簡單的例子:
// hello.js
module.exports = () => 'Hello world'
// hello.test.js
let hello = require('hello.js')
test('should get "Hello world"', () => {
expect(hello()).toBe('Hello world') // 測試成功
// expect(hello()).toBe('Hello') // 測試失敗
})
其中toBe('Hello world')
便是一句斷言( Jest 管它叫 “matcher” ,想了解更多 matcher 請參考文件)。寫完了用例,執行在專案目錄下執行npm test
,即可看到測試結果。
用例的預處理或後處理
有時我們想在測試開始之前進行下環境的檢查、或者在測試結束之後作一些清理操作,這就需要對用例進行預處理或後處理。對測試檔案中所有的用例進行統一的預處理,可以使用 beforeAll()
函式;而如果想在每個用例開始前進行都預處理,則可使用 beforeEach()
函式。至於後處理,也有對應的 afterAll()
和 afterEach()
函式。
如果只是想對某幾個用例進行同樣的預處理或後處理,可以將先將這幾個用例歸為一組。使用 describe()
函式即可表示一組用例,再將上面提到的四個處理函式置於 describe()
的處理回撥內,就實現了對一組用例的預處理或後處理:
describe('test testObject', () => {
beforeAll(() => {
// 預處理操作
})
test('is foo', () => {
expect(testObject.foo).toBeTruthy()
})
test('is not bar', () => {
expect(testObject.bar).toBeFalsy()
})
afterAll(() => {
// 後處理操作
})
})
測試非同步程式碼
非同步程式碼的測試,關鍵點在於告知測試框架測試何時完成,讓其在恰當的時機進行斷言。隨著Babel的盛行,前端的非同步寫法很多都是用 Promise 的形式了,這使得我們可以用 async/await 類似同步的方式寫非同步。下面看下如何針對這種寫法測試:
// promiseHello.js
module.exports = (name) => {
return new Promise((resolve) => {
setTimeout(() => resolve(`Hello ${name}`), 1000)
})
}
// promiseHello.test.js
let promiseHello = require('promiseHello.js')
test('should get "Hello World"', async () => {
const data = await promiseHello('World');
expect(data).toBe('Hello World');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
const data = await promiseHello('World');
expect(data).toBe('Hello World');
} catch (e) {
expect(e).toMatch('error');
}
});
Mock Functions
Mock 函式允許你測試程式碼之間的連線——實現方式包括:擦除函式的實際實現、捕獲對函式的呼叫 ( 以及在這些呼叫中傳遞的引數) 、在使用 new
例項化時捕獲建構函式的例項、允許測試時配置返回值。
使用 mock 函式
假設我們要測試函式 forEach
的內部實現,這個函式為傳入的陣列中的每個元素呼叫一次回撥函式。
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
為了測試此函式,我們可以使用一個 mock 函式,然後檢查 mock 函式的狀態來確保回撥函式如期呼叫。
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// 此 mock 函式被呼叫了兩次
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
屬性
所有的 mock 函式都有這個特殊的 .mock
屬性,它儲存了關於此函式如何被呼叫、呼叫時的返回值的資訊。
// The function was called exactly once
expect(someMockFunction.mock.calls.length).toBe(1);
// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');
// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);
// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toEqual('test');
Mock 的返回值
Mock 函式也可以用於在測試期間將測試值注入程式碼︰
const myMock = jest.fn();
console.log(myMock());
// > undefined
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
在函式連續傳遞風格(functional continuation-passing style)的程式碼中時,Mock 函式也非常有效。 以這種程式碼風格有助於避免複雜的中間操作,便於直觀表現元件的真實意圖,這有利於在它們被呼叫之前,將值直接注入到測試中。
const filterTestFn = jest.fn();
// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
const result = [11, 12].filter(num => filterTestFn(num));
console.log(result);
// > [11]
console.log(filterTestFn.mock.calls);
// > [ [11], [12] ]
大多數現實世界例子中,實際是在依賴的元件上配一個模擬函式並配置它,但手法是相同的。 在這些情況下,儘量避免在非真正想要進行測試的任何函式內實現邏輯。
Mock模擬模組
假定有個從 API 獲取使用者的類。 該類用 axios 呼叫 API 然後返回 data
,其中包含所有使用者的屬性:
// users.js
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
現在,為測試該方法而不呼叫實際 API (使測試變的緩慢與不穩定),我們可以用 jest.mock(...)
函式自動模擬 axios 模組。
一旦模擬axios模組,axios的返回結果就可以被我們隨意模擬值。我們可為 .get
提供一個 mockResolvedValue
,它會返回假資料用於測試。 實際上,我們想讓 axios.get('/users.json') 有個假的 response。
// users.test.js
import axios from 'axios';
import Users from './users';
jest.mock('axios'); // mock模擬模組
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp); // 模擬實際呼叫axios後的返回值
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});
前端自動化測試的價值
近幾年前端工程化的發展風起雲湧,但是前端自動化測試這塊內容大家卻似乎不太重視。雖然專案迭代過程中會有專門的測試人員進行測試,但等他們來進行測試時,程式碼已經開發完成的狀態。與之相比,如果我們在開發過程中就進行了測試,會有如下的好處:
- 保障程式碼質量和功能的實現的完整度
- 提升開發效率,在開發過程中進行測試能讓我們提前發現 bug ,此時進行問題定位和修復的速度自然比開發完再被叫去修 bug 要快許多
- 便於專案維護,後續任何程式碼更新也必須跑通測試用例,即使進行重構或開發人員發生變化也能保障預期功能的實現
當然,凡事都有兩面性,好處雖然明顯,卻並不是所有的專案都值得引入測試框架,畢竟維護測試用例也是需要成本的。對於一些需求頻繁變更、複用性較低的內容,比如活動頁面,讓開發專門抽出人力來寫測試用例確實得不償失。
需要長期維護的專案。它們需要測試來保障程式碼可維護性、功能的穩定性
較為穩定的專案、或專案中較為穩定的部分。給它們寫測試用例,維護成本低
被多次複用的部分,比如一些通用元件和庫函式。因為多處複用,更要保障質量
參考:
前端測試框架 J