jest:
Jest是由Facebook發布的開源的、基於Jasmine的JavaScript單元測試框架。Jest源於Facebook兩年前的構想,用於快速、可靠地測試Web聊天應用。它吸引了公司內部的興趣,Facebook的一名軟件工程師Jeff Morrison半年前又重拾這個項目,改善它的性能,並將其開源。Jest的目標是減少開始測試一個項目所要花費的時間和認知負荷,因此它提供了大部分你需要的現成工具:快速的命令行接口、Mock工具集以及它的自動模塊Mock系統。此外,如果你在尋找隔離工具例如Mock庫,大部分其它工具將讓你在測試中(甚至經常在你的主代碼中)寫一些不盡如人意的樣板代碼,以使其生效。Jest與Jasmine框架的區別是在后者之上增加了一些層。最值得注意的是,運行測試時,Jest會自動模擬依賴。Jest自動為每個依賴的模塊生成Mock,並默認提供這些Mock,這樣就可以很容易地隔離模塊的依賴。
通過npm 下載 jest 指令是:
npm install --save-dev jest
也可以通過yan 下載jest 指令是:
yarn add --dev jest
模擬功能
Mock函數允許您通過擦除函數的實際實現,捕獲對該函數的調用(以及在這些調用中傳遞的參數),捕獲用實例化的構造函數的實例new
以及允許對它們進行測試時配置來測試代碼之間的鏈接。返回值。
有兩種模擬函數的方法:通過創建要在測試代碼中使用的模擬函數,或編寫manual mock
來重寫模塊依賴性。
使用模擬功能
假設我們正在測試一個函數的實現,該函數forEach
為提供的數組中的每個項目調用一個回調。
1 function forEach(items, callback) { 2 for (let index = 0; index < items.length; index++) { 3 callback(items[index]); 4 } 5 }
為了測試此功能,我們可以使用模擬功能,並檢查模擬的狀態以確保按預期方式調用回調。
1 const mockCallback = jest.fn(x => 42 + x); 2 forEach([0, 1], mockCallback); 3 4 // The mock function is called twice 5 expect(mockCallback.mock.calls.length).toBe(2); 6 7 // The first argument of the first call to the function was 0 8 expect(mockCallback.mock.calls[0][0]).toBe(0); 9 10 // The first argument of the second call to the function was 1 11 expect(mockCallback.mock.calls[1][0]).toBe(1); 12 13 // The return value of the first call to the function was 42 14 expect(mockCallback.mock.results[0].value).toBe(42);
.mock
屬性
所有模擬函數都具有此特殊.mock
屬性,該屬性用於保存有關函數調用方式和函數返回內容的數據。該.mock
屬性還跟蹤this
每個調用的值,因此也
1 const myMock = jest.fn(); 2 3 const a = new myMock(); 4 const b = {}; 5 const bound = myMock.bind(b); 6 bound(); 7 8 console.log(myMock.mock.instances); 9 // > [
1 const myMock = jest.fn(); 2 3 const a = new myMock(); 4 const b = {}; 5 const bound = myMock.bind(b); 6 bound(); 7 8 console.log(myMock.mock.instances); 9 // > [ <a>, <b> ]
這些模擬成員在斷言這些函數如何被調用,實例化或它們返回什么的測試中非常有用:
1 // The function was called exactly once 2 expect(someMockFunction.mock.calls.length).toBe(1); 3 4 // The first arg of the first call to the function was 'first arg' 5 expect(someMockFunction.mock.calls[0][0]).toBe('first arg'); 6 7 // The second arg of the first call to the function was 'second arg' 8 expect(someMockFunction.mock.calls[0][1]).toBe('second arg'); 9 10 // The return value of the first call to the function was 'return value' 11 expect(someMockFunction.mock.results[0].value).toBe('return value'); 12 13 // This function was instantiated exactly twice 14 expect(someMockFunction.mock.instances.length).toBe(2); 15 16 // The object returned by the first instantiation of this function 17 // had a `name` property whose value was set to 'test' 18 expect(someMockFunction.mock.instances[0].name).toEqual('test')
模擬返回值
模擬功能還可用於在測試期間將測試值注入代碼中:
1 const myMock = jest.fn(); 2 console.log(myMock()); 3 // > undefined 4 5 myMock 6 .mockReturnValueOnce(10) 7 .mockReturnValueOnce('x') 8 .mockReturnValue(true); 9 10 console.log(myMock(), myMock(), myMock(), myMock()); 11 // > 10, 'x', true, true
模擬函數在使用函數連續傳遞樣式的代碼中也非常有效。用這種風格編寫的代碼有助於避免使用復雜的存根來重新創建其所代表的實際組件的行為,從而有利於在使用它們之前將值直接注入測試中。
1 const filterTestFn = jest.fn(); 2 3 // Make the mock return `true` for the first call, 4 // and `false` for the second call 5 filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false); 6 7 const result = [11, 12].filter(num => filterTestFn(num)); 8 9 console.log(result); 10 // > [11] 11 console.log(filterTestFn.mock.calls); 12 // > [ [11], [12] ]
實際上,大多數實際示例都涉及在依賴組件上獲取模擬功能並對其進行配置,但是技術是相同的。在這種情況下,請嘗試避免在任何未經直接測試的功能內實現邏輯的誘惑。
模擬模塊
假設我們有一個從API獲取用戶的類。該類使用axios調用API,然后返回data
包含所有用戶的屬性:
1 // users.js 2 import axios from 'axios'; 3 4 class Users { 5 static all() { 6 return axios.get('/users.json').then(resp => resp.data); 7 } 8 } 9 10 export default Users;
現在,為了在不實際訪問API的情況下測試該方法(從而創建緩慢而脆弱的測試),我們可以使用該jest.mock(...)
函數自動模擬axios模塊。
一旦對模塊進行了模擬,我們就可以提供一個mockResolvedValue
for .get
,以返回我們要針對測試進行斷言的數據。實際上,我們說的是我們希望axios.get('/ users.json')返回假響應。
1 // users.test.js 2 import axios from 'axios'; 3 import Users from './users'; 4 5 jest.mock('axios'); 6 7 test('should fetch users', () => { 8 const users = [{name: 'Bob'}]; 9 const resp = {data: users}; 10 axios.get.mockResolvedValue(resp); 11 12 // or you could use the following depending on your use case: 13 // axios.get.mockImplementation(() => Promise.resolve(resp)) 14 15 return Users.all().then(data => expect(data).toEqual(users)); 16 });
模擬實現
但是,在某些情況下,超越指定返回值的功能並完全替換模擬功能的實現是有用的。這可以通過模擬函數jest.fn
或mockImplementationOnce
方法來完成。
1 const myMockFn = jest.fn(cb => cb(null, true)); 2 3 myMockFn((err, val) => console.log(val)); 4 // > true
mockImplementation
當您需要定義從另一個模塊創建的模擬函數的默認實現時,該方法很有用:
1 // foo.js 2 module.exports = function() { 3 // some implementation; 4 }; 5 6 // test.js 7 jest.mock('../foo'); // this happens automatically with automocking 8 const foo = require('../foo'); 9 10 // foo is a mock function 11 foo.mockImplementation(() => 42); 12 foo(); 13 // > 42
當您需要重新創建模擬函數的復雜行為以使多個函數調用產生不同的結果時,請使用以下mockImplementationOnce
方法:
1 const myMockFn = jest 2 .fn() 3 .mockImplementationOnce(cb => cb(null, true)) 4 .mockImplementationOnce(cb => cb(null, false)); 5 6 myMockFn((err, val) => console.log(val)); 7 // > true 8 9 myMockFn((err, val) => console.log(val)); 10 // > false
當模擬功能用盡了用定義的實現時mockImplementationOnce
,它將執行的默認實現集jest.fn
(如果已定義):
1 const myMockFn = jest 2 .fn(() => 'default') 3 .mockImplementationOnce(() => 'first call') 4 .mockImplementationOnce(() => 'second call'); 5 6 console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()); 7 // > 'first call', 'second call', 'default', 'default'
對於通常具有鏈式方法(因此總是需要返回this
)的情況,我們提供了一個含糖的API以簡化.mockReturnThis()
函數的形式簡化此過程,該函數也位於所有模擬中:
1 const myObj = { 2 myMethod: jest.fn().mockReturnThis(), 3 }; 4 5 // is the same as 6 7 const otherObj = { 8 myMethod: jest.fn(function() { 9 return this; 10 }), 11 };
模擬名稱
您可以選擇為模擬函數提供一個名稱,該名稱將在測試錯誤輸出中顯示,而不是顯示“ jest.fn()”。如果您希望能夠快速識別在測試輸出中報告錯誤的模擬功能,請使用此功能。
1 const myMockFn = jest 2 .fn() 3 .mockReturnValue('default') 4 .mockImplementation(scalar => 42 + scalar) 5 .mockName('add42');
自定義匹配器
最后,為了減少對如何調用模擬函數的要求,我們為您添加了一些自定義匹配器函數:
1 // The mock function was called at least once 2 expect(mockFunc).toHaveBeenCalled(); 3 4 // The mock function was called at least once with the specified args 5 expect(mockFunc).toHaveBeenCalledWith(arg1, arg2); 6 7 // The last call to the mock function was called with the specified args 8 expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2); 9 10 // All calls and the name of the mock is written as a snapshot 11 expect(mockFunc).toMatchSnapshot();
這些匹配器是用於檢查.mock
財產的常見形式的糖。如果這更符合您的口味,或者您需要執行更具體的操作,則始終可以自己手動執行此操作:
1 // The mock function was called at least once 2 expect(mockFunc.mock.calls.length).toBeGreaterThan(0); 3 4 // The mock function was called at least once with the specified args 5 expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]); 6 7 // The last call to the mock function was called with the specified args 8 expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([ 9 arg1, 10 arg2, 11 ]); 12 13 // The first arg of the last call to the mock function was `42` 14 // (note that there is no sugar helper for this specific of an assertion) 15 expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42); 16 17 // A snapshot will check that a mock was invoked the same number of times, 18 // in the same order, with the same arguments. It will also assert on the name. 19 expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]); 20 expect(mockFunc.getMockName()).toBe('a mock name');
Dom 操作:
通常認為很難測試的另一類功能是直接操作DOM的代碼。讓我們看看如何測試下面的jQuery代碼片段,以偵聽click事件,異步獲取一些數據並設置span的內容。
1 // displayUser.js 2 'use strict'; 3 4 const $ = require('jquery'); 5 const fetchCurrentUser = require('./fetchCurrentUser.js'); 6 7 $('#button').click(() => { 8 fetchCurrentUser(user => { 9 const loggedText = 'Logged ' + (user.loggedIn ? 'In' : 'Out'); 10 $('#username').text(user.fullName + ' - ' + loggedText); 11 }); 12 });
同樣,我們在
__tests__/
文件夾中創建一個測試文件:
1 // __tests__/displayUser-test.js 2 'use strict'; 3 4 jest.mock('../fetchCurrentUser'); 5 6 test('displays a user after a click', () => { 7 // Set up our document body 8 document.body.innerHTML = 9 '<div>' + 10 ' <span id="username" />' + 11 ' <button id="button" />' + 12 '</div>'; 13 14 // This module has a side-effect 15 require('../displayUser'); 16 17 const $ = require('jquery'); 18 const fetchCurrentUser = require('../fetchCurrentUser'); 19 20 // Tell the fetchCurrentUser mock function to automatically invoke 21 // its callback with some data 22 fetchCurrentUser.mockImplementation(cb => { 23 cb({ 24 fullName: 'Johnny Cash', 25 loggedIn: true, 26 }); 27 }); 28 29 // Use jquery to emulate a click on our button 30 $('#button').click(); 31 32 // Assert that the fetchCurrentUser function was called, and that the 33 // #username span's inner text was updated as we'd expect it to. 34 expect(fetchCurrentUser).toBeCalled(); 35 expect($('#username').text()).toEqual('Johnny Cash - Logged In'); 36 });