// my-func.test.js
import { jest } from '@jest/globals';
import { myFunc } from './my-func.js';
import fs from 'node:fs';
const mock = jest.fn().mockReturnValue({isDirectory: () => true});
jest.spyOn(fs, 'statSync').mockImplementation(mock);
it('should work', () => {
expect(myFunc('/test')).toBeTruthy();
expect(mock).toHaveBeenCalled();
});
此实现测试通过:
// my-func.js
import fs from 'node:fs';
// import { statSync } from 'node:fs';
export function myFunc(dir) {
const stats = fs.statSync(dir, { throwIfNoEntry: false });
// const stats = statSync(dir, { throwIfNoEntry: false });
return stats.isDirectory();
}
但是,如果实现更改为(注意不同的导入机制):
// my-func.js
// import fs from 'node:fs';
import { statSync } from 'node:fs';
export function myFunc(dir) {
// const stats = fs.statSync(dir, { throwIfNoEntry: false });
const stats = statSync(dir, { throwIfNoEntry: false });
return stats.isDirectory();
}
Jest 将会失败:
Cannot read properties of undefined (reading 'isDirectory')
TypeError: Cannot read properties of undefined (reading 'isDirectory')
如何使用
statSync
语法监视或模拟 import { statSync } from 'node:fs';
?还可能吗?
注意测试是在ES模块模式下运行的:
# package.json
{
"type": "module",
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
...
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@jest/globals": "^29.7.0",
"globals": "^15.14.0",
"jest": "^29.7.0",
...
},
...
}
编辑:应该早先提到这一点,上面的示例代码是故意修剪的,仅关注我面临的间谍/模拟问题。 超出主题范围的内容:
您更新的实现失败,因为被测代码不再使用您的测试正在监视的
fs
模块对象 - 命名导入直接引入相关函数。然而,这告诉你一些事情:
statSync
返回 undefined
,那么该函数肯定应该返回 false
(或者至少抛出一个更具信息性的错误)?使用
jest.spyOn(fs, 'statSync')
意味着您的测试与正在测试的代码使用 import fs from 'node:fs'
的实现细节相结合。要成功模拟整个
fs
模块,您需要参考 ESM 中的模块模拟。将本指南应用于您的具体案例:
import { jest } from '@jest/globals';
const mock = jest.fn().mockReturnValue({ isDirectory: () => true });
// 1. need to use (unstable!) ES module mocking
jest.unstable_mockModule('node:fs', () => {
const module = { statSync: mock };
// 2. to tolerate default or named import, provide both
return { default: module, ...module };
});
// 3. module needs dynamically importing _after_ mock is registered
const { myFunc } = await import('./my-func.js');
it('should work', () => {
expect(myFunc('/test')).toBeTruthy();
expect(mock).toHaveBeenCalled();
});
注意,默认导出并不总是包含所有命名导出的对象;与往常一样,确保测试替身与它所替换的对象具有相同的接口非常重要。
此测试现在将通过任一实现(使用命名或默认导出来访问
statSync
)。
但是,考虑到您要测试的内容,测试它的另一种方法(这取决于更少的实现细节 - 如果需要,您现在可以使用
statSync
以外的方法)是在本地文件系统上实际尝试。例如,假设它位于传统 npm 项目的根目录中(或者您可以使用具有已知设置的测试装置目录):
import { myFunc } from "./my-func.js";
it('should work for a directory', () => {
expect(myFunc('./node_modules')).toBe(true);
});
it('should work for a file', () => {
expect(myFunc('./package.json')).toBe(false);
});
it('should work for a missing object', () => {
expect(myFunc('./missing')).toBe(false);
// or something like:
// expect(() => myFunc('./missing')).toThrow(/could not be found/);
});
如上所述,这表明在一种情况下行为可能不正确:
FAIL ./my-func.test.js
✓ should work for a directory (1 ms)
✓ should work for a file
✕ should work for a missing object (1 ms)
● should work for a missing object
TypeError: Cannot read properties of undefined (reading 'isDirectory')
Jest 通过替换或监视对象的属性来工作(例如当 fs 作为整体导入时 fs.statSync)。命名导入的问题是没有可用于模拟的对象或属性