所以,我在我的工作区查看测试时,偶然发现了这个奇怪的 “require” 模式,让我感到困惑。
function getMocksForTest(): typeof import('./module-to-test') {
// 我们不能简单地在模块的顶层导入这个,因为必要的 jest mocks 还没有被应用。
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');
});
使用 import 语句要简洁得多。那么为什么使用了不同的模式呢?我刚加入时就让它保持原样,但现在我做了一些研究和软件考古,以更好地理解这个模式的来源、何时需要它以及何时可以移除它。
这个模式仅在使用 jest.mock 时有用#
让我感到困惑的第一件事是名称 getMocksForTest
。没有涉及到模拟,所以这个名称似乎不太合适。但我后来发现了一些设置更复杂的测试文件。
function getMocksForTest() {
const mockCreatePage = jest.fn();
jest.mock('../pages', () => ({
...jest.requireActual('../pages'),
createPage: mockCreatePage,
}));
// 我们不能简单地在模块的顶层导入这个,因为必要的 jest mocks 还没有被应用。
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 () 不总是在 import 语句之后运行吗?#
现在我们知道,当 jest.mock()
在模块导入后运行时需要这个模式。但许多测试文件确实使用 jest.mock()
,而我从未见过这个 require 模式被使用。那么 jest.mock()
通常是如何工作的呢?
事实证明,Jest 在运行测试文件之前会对其进行一些特殊的转换。它不仅将 TypeScript 转换为 JavaScript,还会将 jest.mock()
调用提升到文件顶部,在任何 import 语句之前。这是通过 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>;
只要你的所有模块模拟都可以被提升,使用 import 语句导入你的测试代码是安全的。