所以,我在我的工作區檢查測試時,偶然發現了這個奇怪的「require」模式,讓我感到困惑。
function getMocksForTest(): typeof import('./module-to-test') {
// 我們不能在模組的頂層簡單地導入這個,因為必要的 jest 模擬尚未應用。
return require('./module-to-test');
}
it('通過簡單測試', () => {
const { functionToTest } = getMocksForTest();
expect(functionToTest()).toEqual('foo');
});
在使用 Jest 時,我從未需要切換到 require 語句,所以看到這個我感到驚訝。通常我會直接導入模組。
import { functionToTest } from './module-to-test';
it('通過簡單測試', () => {
expect(functionToTest()).toEqual('foo');
});
使用導入語句要簡潔得多。那麼為什麼使用了不同的模式呢?我在第一次加入時就讓它保持不變,但現在我已經進行了一些研究和軟體考古,以更好地理解這個模式的來源、何時需要它以及何時可以刪除它。
這個模式僅在使用 jest.mock 時有用#
首先讓我感到困惑的是名稱 getMocksForTest
。沒有涉及模擬,所以這個名稱似乎不合適。但我後來發現了一些設置更複雜的測試文件。
function getMocksForTest() {
const mockCreatePage = jest.fn();
jest.mock('../pages', () => ({
...jest.requireActual('../pages'),
createPage: mockCreatePage,
}));
// 我們不能在模組的頂層簡單地導入這個,因為必要的 jest 模擬尚未應用。
const { functionToTest } =
require('./module-to-test') as typeof import('./module-to-test');
return {
functionToTest,
mockCreatePage,
};
}
it('通過複雜測試', () => {
const { functionToTest, mockCreatePage } = getMocksForTest();
mockCreatePage.mockReturnValue('dependency');
expect(functionToTest()).toEqual('foo+dependency');
});
在這裡,我們使用 jest.mock()
來模擬文件 ../pages.ts
中的模組。我們正在測試的模組 ./module-to-test.ts
依賴於 ../pages.ts
並導入它。
如果我們在運行 jest.mock()
之前導入了要測試的模組,那麼我們的模擬將無法及時設置。相反,將導入 ../pages.ts
的原始版本。
import { functionToTest } from './module-to-test';
function getMocksForTest() {
const mockCreatePage = jest.fn();
// 太遲了!`../pages` 在這個時候已經被導入了。
jest.mock('../pages', () => ({
...jest.requireActual('../pages'),
createPage: mockCreatePage,
}));
return {
mockCreatePage,
};
}
現在我遇到了一個實際創建模擬的函數,這個名稱更有意義了。後來,開發人員會複製粘貼測試並僅刪除不需要的部分。這就是為什麼我們最終會得到名為「獲取模擬」的函數,但從未進行任何模擬的原因。
但是 如果測試文件從未使用 jest.mock()
,那麼這個模式完全不必要。可以清理並直接導入模組。
jest.mock()
不總是在導入語句之後運行嗎?#
現在我們知道當 jest.mock()
在模組導入之後運行時需要這個模式。但許多測試文件確實使用 jest.mock()
,而我從未見過這種 require 模式的使用。那麼 jest.mock()
通常是如何工作的呢?
事實上,Jest 在運行測試文件之前會對其進行一些特殊轉換。它不僅將 TypeScript 轉換為 JavaScript,還會將 jest.mock()
調用提升到文件的頂部,在任何導入語句之前。這是通過 babel-jest
插件 完成的,該插件默認啟用。
因此,像這樣的測試文件:
// example.test.ts
import { functionToTest } from './module-to-test';
jest.mock('../pages', () => {
const mockCreatePage: jest.MockedFn<() => string> = jest.fn();
return {
...jest.requireActual('../pages'),
createPage: mockCreatePage,
};
});
it('通過複雜測試', () => {
const { functionToTest, mockCreatePage } = getMocksForTest();
mockCreatePage.mockReturnValue('dependency');
expect(functionToTest()).toEqual('foo+dependency');
});
將被轉換為一個 Node.js 兼容的文件,如下所示:
// compliled.test.js
// 提升到文件的頂部
jest.mock('../pages', () => {
// TypeScript 類型已移除
const mockCreatePage = jest.fn();
return {
...jest.requireActual('../pages'),
createPage: mockCreatePage,
};
});
// 導入語句轉換為 CommonJS
const { functionToTest } = require('./module-to-test');
it('通過複雜測試', () => {
const { functionToTest, mockCreatePage } = getMocksForTest();
mockCreatePage.mockReturnValue('dependency');
expect(functionToTest()).toEqual('foo+dependency');
});
jest.mock
是否總是被提升?#
不!有特定的條件需要滿足,模組模擬才能被提升。我從挖掘 Jest 的 babel 插件的源代碼 中找到了這些檢查。
jest.mock()
的第一個參數應該是字面值。- 如果提供,第二個參數應該是內聯函數。
- 該函數不應引用函數外部定義的任何變數。
jest.mock()
必須在頂層調用,而不是在另一個函數內部。- 否則,它將被移動到調用它的函數的頂部。
基本上,如果你不能簡單地將 jest.mock()
代碼剪切並粘貼到文件的頂部,它將不會自動移動。引用之前定義的其他變數將阻止函數被提升。
// 這些都不會被提升
const moduleName = './module';
jest.mock(moduleName);
function factory() {
return jest.fn();
}
jest.mock('./module', factory);
jest.mock('./module', () => {
// 在範圍外定義的變數
return jest.fn().mockReturnValue(moduleName);
});
如果你確實需要引用在模擬函數外部定義的變數,你可以單獨導入它們或使用 jest.doMock
來明確避免提升。
import { createPage } from '../pages';
jest.mock('../pages', () => ({
createPage: jest.fn(),
}));
const mockCreatePage = createPage as jest.Mocked<typeof createPage>;
只要你的所有模組模擬都可以被提升,使用導入語句導入要測試的代碼是安全的。