手動模擬
手動模擬用於以模擬資料取代功能。例如,您可能想建立一個手動模擬,讓您使用假資料,而不是存取遠端資源,例如網站或資料庫。這可確保您的測試快速且穩定。
模擬使用者模組
手動模擬定義方式為在與模組直接相鄰的 __mocks__/
子目錄中撰寫模組。例如,若要模擬 models
目錄中的 user
模組,請建立一個名為 user.js
的檔案,並將其放入 models/__mocks__
目錄中。
__mocks__
資料夾區分大小寫,因此在某些系統中,將目錄命名為 __MOCKS__
會中斷。
當我們在測試中需要那個模組(表示我們想使用手動模擬而非實際實作)時,明確呼叫 jest.mock('./moduleName')
是必要的。
模擬 Node 模組
如果您要模擬的模組是 Node 模組(例如:lodash
),則應將模擬放置在與 node_modules
相鄰的 __mocks__
目錄中(除非您已設定 roots
指向專案根目錄以外的資料夾),且將自動模擬。無需明確呼叫 jest.mock('module_name')
。
範圍模組(也稱為 範圍套件)可透過在與範圍模組名稱相符的目錄結構中建立檔案來模擬。例如,若要模擬名為 @scope/project-name
的範圍模組,請在 __mocks__/@scope/project-name.js
中建立檔案,並相應建立 @scope/
目錄。
如果我們要模擬 Node 的內建模組(例如:fs
或 path
),則明確呼叫例如 jest.mock('path')
是必要的,因為內建模組預設不會被模擬。
範例
.
├── config
├── __mocks__
│ └── fs.js
├── models
│ ├── __mocks__
│ │ └── user.js
│ └── user.js
├── node_modules
└── views
當給定模組存在手動模擬時,Jest 的模組系統會在明確呼叫 jest.mock('moduleName')
時使用該模組。但是,當 automock
設為 true
時,即使未呼叫 jest.mock('moduleName')
,手動模擬實作也會用於取代自動建立的模擬。若要選擇不使用此行為,您需要在應使用實際模組實作的測試中明確呼叫 jest.unmock('moduleName')
。
為了正確模擬,Jest 需要 jest.mock('moduleName')
與 require/import
陳述式在相同的範圍內。
以下是一個虛構的範例,其中有一個模組提供特定目錄中所有檔案的摘要。在這個案例中,我們使用核心 (內建) fs
模組。
'use strict';
const fs = require('fs');
function summarizeFilesInDirectorySync(directory) {
return fs.readdirSync(directory).map(fileName => ({
directory,
fileName,
}));
}
exports.summarizeFilesInDirectorySync = summarizeFilesInDirectorySync;
由於我們希望測試避免實際存取磁碟 (這相當緩慢且脆弱),我們透過延伸自動模擬來建立 fs
模組的手動模擬。我們的手動模擬將實作 fs
API 的自訂版本,我們可以在測試中建立這些版本
'use strict';
const path = require('path');
const fs = jest.createMockFromModule('fs');
// This is a custom function that our tests can use during setup to specify
// what the files on the "mock" filesystem should look like when any of the
// `fs` APIs are used.
let mockFiles = Object.create(null);
function __setMockFiles(newMockFiles) {
mockFiles = Object.create(null);
for (const file in newMockFiles) {
const dir = path.dirname(file);
if (!mockFiles[dir]) {
mockFiles[dir] = [];
}
mockFiles[dir].push(path.basename(file));
}
}
// A custom version of `readdirSync` that reads from the special mocked out
// file list set via __setMockFiles
function readdirSync(directoryPath) {
return mockFiles[directoryPath] || [];
}
fs.__setMockFiles = __setMockFiles;
fs.readdirSync = readdirSync;
module.exports = fs;
現在我們撰寫測試。在這個案例中,必須明確呼叫 jest.mock('fs')
,因為 fs
是 Node 的內建模組
'use strict';
jest.mock('fs');
describe('listFilesInDirectorySync', () => {
const MOCK_FILE_INFO = {
'/path/to/file1.js': 'console.log("file1 contents");',
'/path/to/file2.txt': 'file2 contents',
};
beforeEach(() => {
// Set up some mocked out file info before each test
require('fs').__setMockFiles(MOCK_FILE_INFO);
});
test('includes all files in the directory in the summary', () => {
const FileSummarizer = require('../FileSummarizer');
const fileSummary =
FileSummarizer.summarizeFilesInDirectorySync('/path/to');
expect(fileSummary.length).toBe(2);
});
});
此處顯示的範例模擬使用 jest.createMockFromModule
來產生自動模擬,並覆寫其預設行為。這是建議的方法,但完全是選用的。如果您完全不想使用自動模擬,您可以從模擬檔案匯出您自己的函式。完全手動模擬的一個缺點是它們是手動的,這表示您必須在模擬的模組變更時手動更新它們。因此,當它符合您的需求時,最好使用或延伸自動模擬。
為了確保手動模擬及其實際實作保持同步,在您的手動模擬中使用 jest.requireActual(moduleName)
來要求實際模組,並在匯出之前使用模擬函式修正它,這可能會很有用。
此範例的程式碼可在 examples/manual-mocks 取得。
使用 ES 模組匯入
如果您使用 ES 模組匯入,您通常會傾向於將 import
陳述式放在測試檔案的最上方。但您通常需要指示 Jest 在模組使用模擬之前使用模擬。因此,Jest 會自動將 jest.mock
呼叫提升到模組的最上方 (在任何匯入之前)。若要進一步瞭解這一點並實際操作,請參閱 此儲存庫。
如果您啟用了 ECMAScript 模組支援,則無法將 jest.mock
呼叫提升到模組的最上方。ESM 模組載入器總是在執行程式碼之前評估靜態匯入。請參閱 ECMAScriptModules 以取得詳細資訊。
模擬 JSDOM 中未實作的方法
如果某些程式碼使用 JSDOM(Jest 使用的 DOM 實作)尚未實作的方法,則無法輕易地測試它。例如,window.matchMedia()
的情況。Jest 會傳回 TypeError: window.matchMedia is not a function
,而且不會正確執行測試。
在此情況下,在測試檔案中模擬 matchMedia
應可解決問題
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
如果 window.matchMedia()
用於測試中呼叫的函式(或方法)中,則此方法有效。如果 window.matchMedia()
直接在受測檔案中執行,Jest 會報告相同的錯誤。在此情況下,解決方案是將手動模擬移至獨立的檔案,並在測試中於受測檔案之前包含此檔案
import './matchMedia.mock'; // Must be imported before the tested file
import {myMethod} from './file-to-test';
describe('myMethod()', () => {
// Test the method here...
});