如何使用 Jest 以 ESModule 方式模拟/监视命名导入?

问题描述 投票:0回答:2
// 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",
    ...
  },
  ...
}

编辑:应该早先提到这一点,上面的示例代码是故意修剪的,仅关注我面临的间谍/模拟问题。 超出主题范围的内容:

  • 错误/异常处理。
  • 非嘲笑/间谍替代品。
javascript node.js jestjs
2个回答
1
投票

您更新的实现失败,因为被测代码不再使用您的测试正在监视的

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')

-1
投票

Jest 通过替换或监视对象的属性来工作(例如当 fs 作为整体导入时 fs.statSync)。命名导入的问题是没有可用于模拟的对象或属性

© www.soinside.com 2019 - 2024. All rights reserved.