近年來,隨着前端工程化的發展,前端發生了翻天覆地的變化。jQuery已經慢慢淡出了咱們的視野,React、Vue和anglur三駕馬車急速駛來。今後,前端進入了數據驅動的時代,也有了清晰的模塊化開發的方式。隨之而來的就是如何去保證本身的代碼的正確性。html
編寫測試代碼要在正是寫代碼前進行的,它就至關於具體明確的需求文檔。以後咱們寫的代碼若是能經過測試代碼就證實是符合預期的。前端
除此以外,因爲一個項目須要多人維護,也許別人不當心改動了你的代碼就會致使新的問題。因此提交代碼前須要跑一遍測試用例,確保本身沒有改動別人的邏輯。若是有改動別人的代碼,必定要弄清楚這樣改動會不會產生新的問題,最後記得把測試用例代碼也要改下。ios
前端測試工具也和前端的框架同樣紛繁複雜,其中常見的測試工具,大體可分爲測試框架、斷言庫、測試覆蓋率工具等幾類。在正式開始本文以前,咱們先來大體瞭解下它們:git
測試框架的做用是提供一些方便的語法來描述測試用例,以及對用例進行分組。github
測試框架可分爲兩種: TDD (測試驅動開發)和 BDD (行爲驅動開發),我理解二者間的區別主要是一些語法上的不一樣,其中 BDD 提供了提供了可讀性更好的用例語法,至於詳細的區別可參見 The Difference Between TDD and BDD 一文。npm
常見的測試框架有 Jasmine, Mocha 以及本文要介紹的 Jest 。json
斷言庫主要提供語義化方法,用於對參與測試的值作各類各樣的判斷。這些語義化方法會返回測試的結果,要麼成功、要麼失敗。常見的斷言庫有 Should.js, Chai.js 等。axios
用於統計測試用例對代碼的測試狀況,生成相應的報表,好比 istanbul 。前端工程化
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 函數容許你測試代碼之間的鏈接——實現方式包括:擦除函數的實際實現、捕獲對函數的調用 ( 以及在這些調用中傳遞的參數) 、在使用 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] ]
大多數現實世界例子中,實際是在依賴的組件上配一個模擬函數並配置它,但手法是相同的。 在這些狀況下,儘可能避免在非真正想要進行測試的任何函數內實現邏輯。
假定有個從 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)); });
近幾年前端工程化的發展風起雲涌,可是前端自動化測試這塊內容你們卻彷佛不過重視。雖然項目迭代過程當中會有專門的測試人員進行測試,但等他們來進行測試時,代碼已經開發完成的狀態。與之相比,若是咱們在開發過程當中就進行了測試,會有以下的好處:
固然,凡事都有兩面性,好處雖然明顯,卻並非全部的項目都值得引入測試框架,畢竟維護測試用例也是須要成本的。對於一些需求頻繁變動、複用性較低的內容,好比活動頁面,讓開發專門抽出人力來寫測試用例確實得不償失。
須要長期維護的項目。它們須要測試來保障代碼可維護性、功能的穩定性
較爲穩定的項目、或項目中較爲穩定的部分。給它們寫測試用例,維護成本低
被屢次複用的部分,好比一些通用組件和庫函數。由於多處複用,更要保障質量
參考: