如何使用Jest模拟`fs.promises.writeFile`函数

11

我试图使用Jest来模拟fs.writeFilepromise版本,并且模拟的函数没有被调用。

待测试的函数(createFile.js):

const { writeFile } = require("fs").promises;

const createNewFile = async () => {
    await writeFile(`${__dirname}/newFile.txt`, "Test content");
};

module.exports = {
    createNewFile,
};

Jest测试 (createFile.test.js):

const fs = require("fs").promises;
const { createNewFile } = require("./createFile.js");

it("Calls writeFile", async () => {
    const writeFileSpy = jest.spyOn(fs, "writeFile");

    await createNewFile();
    expect(writeFileSpy).toHaveBeenCalledTimes(1);

    writeFileSpy.mockClear();
});

我知道writeFile实际上被调用了,因为我运行了node -e "require(\"./createFile.js\").createNewFile()",并且文件已经创建。

依赖版本

  • Node.js: 14.1.0
  • Jest: 26.6.3

-- 这是对createFile.test.js文件的另一次尝试 --

const fs = require("fs");
const { createNewFile } = require("./createFile.js");

it("Calls writeFile", async () => {
    const writeFileMock = jest.fn();

    jest.mock("fs", () => ({
        promises: {
            writeFile: writeFileMock,
        },
    }));

    await createNewFile();
    expect(writeFileMock).toHaveBeenCalledTimes(1);
});

这会抛出以下错误:

ReferenceError: /Users/danlevy/Desktop/test/src/createFile.test.js: The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables.
    Invalid variable access: writeFileMock
3个回答

15

由于在导入时 writeFile 被解构而不是一直被引用为 fs.promises.writeFile 方法,因此它不能受到 spyOn 的影响。

它应该像任何其他模块一样被模拟:


jest.mock("fs", () => ({
  promises: {
    writeFile: jest.fn().mockResolvedValue(),
    readFile: jest.fn().mockResolvedValue(),
  },
}));

const fs = require("fs");

...

await createNewFile();
expect(fs.promises.writeFile).toHaveBeenCalledTimes(1);

仅在极少数情况下进行mock fs 是有道理的,因为未被mock的函数会产生副作用并潜在地对测试环境造成负面影响。


在这种情况下,我该如何访问writeFile的模拟版本,以便我可以调用类似于toHaveBeenCalled(..)的模拟函数?我尝试将writeFile简单地设置为jest.mock(..)之外的变量,但是我会收到以下错误:jest.mock()的模块工厂不允许引用任何超出范围的变量,其中超出范围的变量是jest.mock(..)之外的变量。请参见我的原始问题中的尝试。 - Dan Levy
错误试图保护您免受访问在使用时可能未声明的变量,因为jest.mock是被提升的,您可以将变量命名为let mockWriteFile = jest.fn()...但风险自负,请参见此处的解释https://jestjs.io/docs/en/es6-class-mocks#calling-jestmockdocsenjest-objectjestmockmodulename-factory-options-with-the-module-factory-parameter。应该在测试文件中导入`fs`,这样就可以访问`fs.promises.writeFile`进行断言。 - Estus Flask
我也尝试在测试文件中导入了 fs,但仍然出现了相同的错误。请参见 OP 中更新的代码。 - Dan Levy
好的,我明白你所说的关于提升的内容。感谢提供链接!那么归根结底,有没有一个好的解决方案来实现我最初尝试做的事情(仅使用Jest而不使用mock-fs等其他工具,例如简单地检查是否调用了writeFile)? - Dan Levy
1
jest.mock 应该位于顶层,否则它在影响到的导入之前就没有机会被评估。导入 fs 的目的是为了在测试范围内引用它,而不是 writeFileSpy 变量,因为这样可以有效地使 writeFileSpy === fs.promises.writeFile。我已经更新了帖子以提高清晰度。 - Estus Flask

6

在Jest中模拟"fs/promises"异步函数

这里有一个使用fs.readdir()的简单示例,但它也适用于任何其他异步fs/promises函数。

files.service.test.js

import fs from "fs/promises";
import FileService from "./files.service";

jest.mock("fs/promises");

describe("FileService", () => {
  var fileService: FileService;

  beforeEach(() => {
    // Create a brand new FileService before running each test
    fileService = new FileService();

    // Reset mocks
    jest.resetAllMocks();
  });

  describe("getJsonFiles", () => {
    it("throws an error if reading the directory fails", async () => {
      // Mock the rejection error
      fs.readdir = jest.fn().mockRejectedValueOnce(new Error("mock error"));

      // Call the function to get the promise
      const promise = fileService.getJsonFiles({ folderPath: "mockPath", logActions: false });

      expect(fs.readdir).toHaveBeenCalled();
      await expect(promise).rejects.toEqual(new Error("mock error"));
    });

    it("returns an array of the .json file name strings in the test directory (and not any other files)", async () => {
      const allPotentialFiles = ["non-json.txt", "test-json-1.json", "test-json-2.json"];
      const onlyJsonFiles = ["test-json-1.json", "test-json-2.json"];

      // Mock readdir to return all potential files from the dir
      fs.readdir = jest.fn().mockResolvedValueOnce(allPotentialFiles);

      // Get the promise
      const promise = fileService.getJsonFiles({ folderPath: "mockPath", logActions: false });

      expect(fs.readdir).toBeCalled();
      await expect(promise).resolves.toEqual(onlyJsonFiles); // function should only return the json files
    });
  });
});

files.service.ts

import fs from "fs/promises";

export default class FileService {
  constructor() {}

  async getJsonFiles(args: FilesListArgs): Promise<string[]> {
    const { folderPath, logActions } = args;
    try {
      // Get list of all files
      const files = await fs.readdir(folderPath);

      // Filter to only include JSON files
      const jsonFiles = files.filter((file) => {
        return file.includes(".json");
      });

      return jsonFiles;
    } catch (e) {
      throw e;
    }
  }
}

3

我知道这是一篇旧帖子,但在我的情况下,我想处理从 readFile (或者你的情况下的 writeFile)返回的不同结果。所以我使用了 Estus Flask 建议的解决方案,但与其不同的是我在每个测试中处理了 readFile 的每个实现,而不是使用 mockResolvedValue

我还在使用 TypeScript。

import { getFile } from './configFiles';

import fs from 'fs';
jest.mock('fs', () => {
  return {
    promises: {
      readFile: jest.fn()
    }
  };
});

describe('getFile', () => {
   beforeEach(() => {
      jest.resetAllMocks();
   });

   it('should return results from file', async () => {
      const mockReadFile = (fs.promises.readFile as jest.Mock).mockImplementation(async () =>
        Promise.resolve(JSON.stringify('some-json-value'))
      );

      const res = await getFile('some-path');

      expect(mockReadFile).toHaveBeenCalledWith('some-path', { encoding: 'utf-8' });

      expect(res).toMatchObject('some-json-value');
   });

   it('should gracefully handle error', async () => {
      const mockReadFile = (fs.promises.readFile as jest.Mock).mockImplementation(async () =>
        Promise.reject(new Error('not found'))
      );

      const res = await getFile('some-path');

      expect(mockReadFile).toHaveBeenCalledWith('some-path', { encoding: 'utf-8' });

      expect(res).toMatchObject('whatever-your-fallback-is');
   });
});

请注意,我必须将fs.promises.readFile强制转换为jest.Mock才能使它在TS中工作。
另外,我的configFiles.ts看起来像这样:
import { promises as fsPromises } from 'fs';

const readConfigFile = async (filePath: string) => {
  const res = await fsPromises.readFile(filePath, { encoding: 'utf-8' });
  return JSON.parse(res);
};

export const getFile = async (path: string): Promise<MyType[]> => {
  try {
    const fileName = 'some_config.json';
    return readConfigFile(`${path}/${fileName}`);
  } catch (e) {
    // some fallback value
    return [{}];
  }
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接