
本文详解如何在 TypeScript + Jest 环境中精准 Mock fs/promises 模块,解决因导入方式不匹配导致的 mock 失效问题,并确保 readdir 等方法被真实调用、可断言。
本文详解如何在 typescript + jest 环境中精准 mock `fs/promises` 模块,解决因导入方式不匹配导致的 mock 失效问题,并确保 `readdir` 等方法被真实调用、可断言。
在 Jest 单元测试中 Mock Node.js 内置模块(如 fs/promises)时,一个常见却极易被忽视的关键点是:mock 的模块路径必须与实际导入路径完全一致。你当前的问题根源正在于此——你的生产代码使用的是:
import { promises as fs } from 'fs'; // ← 导入自 'fs' 模块的命名导出
而你在测试中 mock 的却是:
jest.mock('fs/promises'); // ← mock 的是独立的 'fs/promises' 模块
由于 'fs' 和 'fs/promises' 在 Node.js 中是两个不同的模块标识符(尽管内容高度相关),Jest 的模块系统不会将对 'fs' 的导入重定向到对 'fs/promises' 的 mock。因此,calculateMetrics 中调用的 fs.readdir 仍是原生实现,你的 mock 函数自然不会被执行,expect(readdirMock).toHaveBeenCalledTimes(1) 也就必然失败。
✅ 正确做法是 保持导入路径与 mock 路径严格统一。推荐采用以下标准方案:
使用 Vite 8、React 19、Tailwind CSS v4、shadcn/ui、Biome、Vitest 和 Hono 构建全栈 TypeScript 应用,涵盖前端(Vite/Rolldown 构建 + 开发)...
1. 统一使用 fs/promises 导入(推荐)
修改 tradeEvaluator.ts,显式从 'fs/promises' 导入:
// ✅ 正确:与 mock 路径一致
import * as fs from 'fs/promises';
import { join } from 'path';
import { createLogger, format, transports } from 'winston';
export async function calculateMetrics(directory: string, tickerSymbol: string | null = null): Promise<void> {
try {
const files = await fs.readdir(directory); // ← 现在会命中 mock
if (!files.length) {
console.error(`No files found in directory ${directory}`);
return;
}
// ... 其他逻辑
} catch (e) {
console.error(`Error reading directory ${directory}:`, e);
}
}</void>
2. 在测试中正确 mock 并类型化
tradeEvaluator.test.ts 应如下组织(注意顺序与类型断言):
// ✅ 先 mock,再导入被测模块(确保 mock 生效)
jest.mock('fs/promises');
// ✅ 导入方式必须与生产代码一致
import { calculateMetrics } from './tradeEvaluator';
import * as fs from 'fs/promises';
describe('calculateMetrics', () => {
// 类型安全地获取 mock 实例
const readdirMock = fs.readdir as jest.MockedFunction<typeof fs.readdir>;
beforeEach(() => {
// 清理并预设返回值
readdirMock.mockClear();
readdirMock.mockResolvedValue(['file1.json', 'file2.json']);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should call fs.readdir exactly once', async () => {
await calculateMetrics('./data');
expect(readdirMock).toHaveBeenCalledTimes(1);
expect(readdirMock).toHaveBeenCalledWith('./data');
});
it('should handle empty directory gracefully', async () => {
readdirMock.mockResolvedValue([]);
await calculateMetrics('./empty');
expect(readdirMock).toHaveBeenCalledTimes(1);
// 可进一步断言日志或行为...
});
});</typeof>
⚠️ 关键注意事项
- mock 必须在任何相关 import 之前执行:jest.mock(...) 是 hoisted(提升)的,但为清晰起见,建议置于文件顶部。
- 避免混合导入:不要在同一个项目中混用 import { promises as fs } from 'fs' 和 import * as fs from 'fs/promises',否则 mock 管理将混乱。
- 类型安全提示:使用 as jest.MockedFunction<...> 显式断言类型,避免 TS 类型推导错误导致 .mockResolvedValue() 等方法不可用。
- 清理习惯:beforeEach 中调用 mockClear() 或 afterEach 中调用 jest.clearAllMocks(),防止测试间状态污染。
✅ 验证 mock 是否生效
可在测试中添加调试日志确认:
console.log('readdir is mocked:', typeof readdirMock === 'function' && (readdirMock as any)._isMockFunction);
// 输出 true 表示 mock 成功
遵循以上规范,即可确保 fs.promises 相关方法被可靠拦截、可控模拟,并支持完整的调用次数、参数及返回值断言,大幅提升文件操作类逻辑的测试覆盖率与可靠性。










