【问题标题】:How to Mock `fs.promises.writeFile` with Jest如何用 Jest 模拟`fs.promises.writeFile`
【发布时间】:2021-03-04 22:48:36
【问题描述】:

我正在尝试使用 Jest 模拟 fs.writeFilepromise 版本,但未调用模拟函数。

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

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

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

module.exports = {
    createNewFile,
};

玩笑测试 (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
  • 开玩笑: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

【问题讨论】:

    标签: node.js jestjs mocking fs spy


    【解决方案1】:

    由于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);
    

    模拟fs 几乎没有意义,因为未模拟的函数会产生副作用并可能对测试环境产生负面影响。

    【讨论】:

    • 在这种情况下,我将如何访问 writeFile 的模拟版本,以便我可以在模拟函数上调用类似 toHaveBeenCalled(..) 的东西?我尝试简单地将writeFile 设置为jest.mock(..) 之外的变量,但出现以下错误:The module factory of "jest.mock()" is not allowed to reference any out-of-scope variables 范围外变量是jest.mock(..) 之外的变量。请参阅我在原始问题中的尝试。
    • 该错误试图保护您不访问在使用时可能未声明的变量,因为 jest.mock 被提升,您可以将变量命名为let mockWriteFile = jest.fn()...,但风险自负,看这里的解释jestjs.io/docs/en/…fs 应该在测试文件中导入,这样可以访问 fs.promises.writeFile 进行断言。
    • 我也尝试在测试文件中导入fs,但仍然出现同样的错误。请参阅 OP 中的更新代码。
    • 好的,我明白你对提升的看法。感谢您的链接!因此,归根结底,是否有一个很好的解决方案来做我最初想做的事情(只需检查 writeFile 是否被调用)只使用 Jest 而没有其他东西(例如不使用 mock-fs)?
    • jest.mock 应该位于顶层,否则它没有机会在它应该影响的导入之前进行评估。导入fs 的目的是在测试范围内引用它而不是writeFileSpy 变量,因为这样可以有效地实现writeFileSpy === fs.promises.writeFile。为了清楚起见,我更新了帖子。
    【解决方案2】:

    我知道这是一个旧线程,但就我而言,我想处理来自 readFile(或 writeFile 在您的情况下)的不同结果。所以我使用了 Estus Flask 建议的解决方案,不同之处在于我在每个测试中处理 readFile 的每个实现,而不是使用 mockResolvedValue

    我也在使用打字稿。

    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 [{}];
      }
    };
    

    【讨论】:

      【解决方案3】:

      在玩笑中模拟“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;
          }
        }
      }
      

      【讨论】:

        猜你喜欢
        • 2018-12-25
        • 2021-02-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-05-29
        • 1970-01-01
        • 1970-01-01
        • 2018-01-26
        相关资源
        最近更新 更多