tschaub / mock-fs

Configurable mock for the fs module
https://npmjs.org/package/mock-fs
Other
906 stars 86 forks source link

Fix compatibility with Node 20 #384

Open ademdinarevic opened 8 months ago

ademdinarevic commented 8 months ago

Since we upgraded to node 20.10.0 the method for mocking file creation doesn't work properly

const mockFs = require("mock-fs");

mockFs({
  "/file": {
     VAR: "var_value",
  },
});
zbigg commented 8 months ago

For anyone trying to fix it, it's caused by node20 to use new binding method: readFileUtf8 which isn't "mocked" by this module.

It looks like we should just implement open+read+toUtf+close as one function here.

The usage:

https://github.com/nodejs/node/blob/a81788cb27b96579343765eeba07fd2b772829d0/lib/fs.js#L456

Implementation

https://github.com/nodejs/node/blob/a81788cb27b96579343765eeba07fd2b772829d0/src/node_file.cc#L2375

This is very naive and PoC binding that fixes readFileSync(..., 'utf8') for node 20

Binding.prototype.readFileUtf8 = function (
  name,
  flags
) {
    const fd = this.open(name, flags);
    const descriptor = this.getDescriptorById(fd);

    if (!descriptor.isRead()) {
      throw new FSError('EBADF');
    }
    const file = descriptor.getItem();
    if (file instanceof Directory) {
      throw new FSError('EISDIR');
    }
    if (!(file instanceof File)) {
      // deleted or not a regular file
      throw new FSError('EBADF');
    }
    const content = file.getContent();
    return content.toString('utf8');
};
canhassancode commented 8 months ago

Facing the exact same issue, it was working completely fine then randomly broke. Below is an example of the setup:

beforeAll(() => {
  mockFs.restore();
});

afterEach(() => {
  mockFs.restore();
});

it('mock test', () => {
  mockFs({
    './profiles': {
      test: {
        'TEST_PROFILE_1.json': JSON.stringify(testProfile1),
        'TEST_PROFILE_2.json': JSON.stringify(testProfile2),
      },
...
    },
  });
});

Getting the error: ENOENT: no such file or directory, open 'profiles/test/TEST_PROFILE_2.json'

crfrolik commented 7 months ago

For those like me who came here in a panic because their tests started failing, I had success (and minimal effort) replacing mock-fs with memfs.

PedroS11 commented 6 months ago

For those like me who came here in a panic because their tests started failing, I had success (and minimal effort) replacing mock-fs with memfs.

Do you mind post some examples please? I'm trying to use memfs but I'm struggling with it. I found an basic example with jest however, i'm using chai so nothing is working

For reference, this is the basic example of memfs using jest: https://medium.com/nerd-for-tech/testing-in-node-js-easy-way-to-mock-filesystem-883b9f822ea4

or https://dev.to/julienp/node-js-testing-using-a-virtual-filesystem-as-a-mock-2jln

JBloss1517 commented 6 months ago

@PedroS11 I also decided to switch using memfs. For me, it was mostly a matter of switching mockFS({. . .}) with vol.fromNestedJSON({. . .}) and changing mockFS.restore() to vol.reset()

So for example (I am using vitest in this but jest should be pretty close as well)

Using mock-fs

const mockFS = require("mock-fs");
import { afterEach, beforeEach, describe, expect, test } from "vitest";

describe("some test", () => {
  beforeEach(() => {
    mockFS({
      Projects: {
        "Projects - 23": {
          "Test Client": {
            "Projects 1": {
              "Sub folder": {},
            },
          },
        },
        "Projects - 24": {},
      },
    });
  });

  afterEach(() => {
    mockFS.restore();
  });

  test("project folder creation", () => {
    const projectFolderPath = path.join(
      "Projects",
      "Projects - 23",
      "Test Client",
      "Projects 2",
    );
    const result = _createProjectFolder(projectFolderPath, projectTemplatePath);
    expect(result).toEqual({ path: projectFolderPath, error: null });
  });
});

Using memfs

import { fs, vol } from "memfs";
import { afterEach, beforeEach, describe, expect, test } from "vitest";

describe("some test", () => {
  beforeEach(() => {
    vol.fromNestedJSON({   // <-- Changed this from mockFS
      Projects: {
        "Projects - 23": {
          "Test Client": {
            "Projects 1": {
              "Sub folder": {},
            },
          },
        },
        "Projects - 24": {},
      },
    });
  });

  afterEach(() => {
    vol.reset(); // <-- changed this from mockFS.restore();
  });

  test("project folder creation", () => {
    const projectFolderPath = path.join(
      "Projects",
      "Projects - 23",
      "Test Client",
      "Projects 2",
    );
    const result = _createProjectFolder(projectFolderPath, projectTemplatePath);
    expect(result).toEqual({ path: projectFolderPath, error: null });
  });
});

I hope that helps!

PedroS11 commented 6 months ago

@PedroS11 I also decided to switch using memfs. For me, it was mostly a matter of switching mockFS({. . .}) with vol.fromNestedJSON({. . .}) and changing mockFS.restore() to vol.reset()

So for example (I am using vitest in this but jest should be pretty close as well)

Using mock-fs

const mockFS = require("mock-fs");
import { afterEach, beforeEach, describe, expect, test } from "vitest";

describe("some test", () => {
  beforeEach(() => {
    mockFS({
      Projects: {
        "Projects - 23": {
          "Test Client": {
            "Projects 1": {
              "Sub folder": {},
            },
          },
        },
        "Projects - 24": {},
      },
    });
  });

  afterEach(() => {
    mockFS.restore();
  });

  test("project folder creation", () => {
    const projectFolderPath = path.join(
      "Projects",
      "Projects - 23",
      "Test Client",
      "Projects 2",
    );
    const result = _createProjectFolder(projectFolderPath, projectTemplatePath);
    expect(result).toEqual({ path: projectFolderPath, error: null });
  });
});

Using memfs

import { fs, vol } from "memfs";
import { afterEach, beforeEach, describe, expect, test } from "vitest";

describe("some test", () => {
  beforeEach(() => {
    vol.fromNestedJSON({   // <-- Changed this from mockFS
      Projects: {
        "Projects - 23": {
          "Test Client": {
            "Projects 1": {
              "Sub folder": {},
            },
          },
        },
        "Projects - 24": {},
      },
    });
  });

  afterEach(() => {
    vol.reset(); // <-- changed this from mockFS.restore();
  });

  test("project folder creation", () => {
    const projectFolderPath = path.join(
      "Projects",
      "Projects - 23",
      "Test Client",
      "Projects 2",
    );
    const result = _createProjectFolder(projectFolderPath, projectTemplatePath);
    expect(result).toEqual({ path: projectFolderPath, error: null });
  });
});

I hope that helps!

Thank you for this snippet @JBloss1517 !! in the following weeks, I'll try to migrate too and I'll see how it works. I'll leave my progress here when I start so it helps everyone with this struggle

dennismeister93 commented 5 months ago

hi @tschaub, are there any plans to work on this?

tschaub commented 5 months ago

@dennismeister93 - I would be happy to review a pull request, but don't have plans to spend time on a fix myself.

dicolasi commented 4 months ago

struggling with the same problem here.

MichaelSitter commented 3 months ago

I was having issues with my Jest tests still using the built-in fs instead of the memfs implementation. This worked to mock out fs & fs/promises:

import { fs, vol } from 'memfs'
// import needs to match what you used in your code
jest.mock('fs', () => fs)
jest.mock('fs/promises', () => fs.promises)

// test code
bcass commented 2 months ago

Here's an example of migrating a mock-fs test that used mock.load() to memfs. The test itself doesn't change. Just adding a vi.mock() and changing the beforeEach() and afterEach().

Before with mock-fs:

import mock from 'mock-fs'
import fs from 'fs'
import { describe, test, expect, beforeEach, afterEach } from 'vitest'

describe('test', () => {
  beforeEach(() => {
    mock({
      test: {
        'UnitTestInput.xlsx': mock.load(path.resolve(__dirname, '../test/UnitTestInput.xlsx'), { lazy: false })
        another: {
          path: {
            'data.json': '{ "fakeData": "yes" }'
          }
        }
      }
    })
  })
  afterEach(() => {
    mock.restore()
  })
  test('myUtility.run() processes test file and creates output', () => {
    // given:
    const myUtility = new MyUtility()

    // when: we run the utility
    myUtility.run(`${process.cwd()}/test/UnitTestInput.xlsx`)

    // then: output is generated and matches snapshot
    expect(fs.readFileSync(`${process.cwd()}/test/another/path/data-generated.json`, 'utf8')).toMatchSnapshot()
    expect(fs.existsSync(`${process.cwd()}/test/report-UnitTestInput.txt`)).toBeTruthy()
  })
})

After with memfs:

import { vol, fs } from "memfs";
import { vi, describe, test, expect, beforeEach, afterEach } from 'vitest'

// Mock fs everywhere else with the memfs version.
vi.mock('fs', async () => {
  const memfs = await vi.importActual('memfs')

  // Support both `import fs from "fs"` and "import { readFileSync } from "fs"`
  return { default: memfs.fs, ...memfs.fs }
})

describe('test', () => {
  beforeEach(async () => {
    // Get the real fs method, read the real file, and inject into the memory file system.
    const fs = await vi.importActual('fs')
    const unitTestInputXlsx = fs.readFileSync(path.resolve(__dirname, '../test/UnitTestInput.xlsx'))

    vol.fromNestedJSON({
      test: {
        'UnitTestInput.xlsx': unitTestInputXlsx,
        another: {
          path: {
            'data.json': '{ "fakeData": "yes" }'
          }
        }
      }
    })
  })
  afterEach(() => {
    vol.reset()
  })
  test('myUtility.run() processes test file and creates output', () => {
    // given:
    const myUtility = new MyUtility()

    // when: we run the utility
    myUtility.run(`${process.cwd()}/test/UnitTestInput.xlsx`)

    // then: output is generated and matches snapshot
    expect(fs.readFileSync(`${process.cwd()}/test/another/path/data-generated.json`, 'utf8')).toMatchSnapshot()
    expect(fs.existsSync(`${process.cwd()}/test/report-UnitTestInput.txt`)).toBeTruthy()
  })
})
sketchbuch commented 1 month ago

I was having issues with my Jest tests still using the built-in fs instead of the memfs implementation. This worked to mock out fs & fs/promises:

import { fs, vol } from 'memfs'
// import needs to match what you used in your code
jest.mock('fs', () => fs)
jest.mock('fs/promises', () => fs.promises)

// test code

What is the mocha equiv. of jest.mock?

BadIdeaException commented 1 month ago

In mocha there is a little more work required, because it does not include a mocking system of its own. Usually you use the excellent Sinon for mocking, but in this special case this won't work since, by specification, imports must be unalterable. Instead, I use esmock to mock out modules. Something like this works for me:

// Note we DO NOT import the system under test here
import { vol } from 'memfs';

describe('test', function() {
    let sut;
    before(async function() {
        sut = (await esmock('../path/to/sut.js', {}, {
            'fs': vol,
            'fs/promises': vol.promisesApi
        })).default;
    });

    beforeEach(function() {
        vol.fromNestedJSON({ ... });
    });

    afterEach(function() {
        vol.reset();
    });

    it('should do something with the sut', async function() {
        const result = await sut.doSomething(); // Even works if the sut imported 'fs/promises' like every sane person does
        expect(result).to.be.something.really.nice;
    });
});