tschaub / mock-fs

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

Dynamic imports are unexpectedly coupled between tests #340

Open mtmacdonald opened 2 years ago

mtmacdonald commented 2 years ago

I'm trying to use mock-fs to unit test code which uses ES6 dynamic imports.

There seems to an unexpected coupling between tests when I'm using dynamic imports, even when I call restore() after each test. It appears as though fs.readFile() behaves as expected between tests (no coupling), but await import() has coupling (it returns the result from the previous test).

I've created a minimal Jest test case that reproduces the issue. The tests pass individually, but not when run together. I notice that if I change the directory value so it's different between each test, then they pass together.

Can you help me understand why this doesn't work, whether it's a bug, and what I should do here?

import path from 'path';
import { promises as fs } from 'fs';
import mockFs from 'mock-fs';

const fsMockModules = {
  node_modules: mockFs.load(path.resolve(__dirname, '../node_modules')),
};

describe('Reproduce dynamic import coupling between tests', () => {
  afterEach(() => {
    mockFs.restore();
  });
  it('first test', async () => {
    const directory = 'some/path';
    mockFs({
      ...fsMockModules,
      [directory]: {
        'index.js': ``,
      },
    });
    await import(path.resolve(`${directory}/index.js`));
    //not testing anything here, just illustrating the coupling for next test
  });
  it('second tests works in isolation but not together with first test', async () => {
    const directory = 'some/path';
    mockFs({
      ...fsMockModules,
      [directory]: {
        'index.js': `export {default as migrator} from './migrator.js';`,
        'migrator.js':
          'export default (payload) => ({...payload, xyz: 123});',
      },
    });
    const indexFile = await fs.readFile(`${directory}/index.js`, 'utf-8');
    expect(indexFile.includes('export {default as migrator}')).toBe(true);
    const migrations = await import(path.resolve(`${directory}/index.js`));
    expect(typeof migrations.migrator).toBe('function');
  });
});
3cp commented 2 years ago

When nodejs calls import() on the same file twice, it will reuse loaded module from 1st call for the 2nd call, same as you do require() twice in CommonJS code before.

Here is the approve, inside a folder, create two files:

  1. package.json

    { "type": "module" }
  2. foo.js

    console.log("loading foo");
    export default "FOOooOO";

Then in nodejs REPL command line, create a function test() to import foo and print out result. You can see the first import() triggered the console log "loading foo" in the foo.js, but second import() didn't log it again, means nodejs didn't read foo.js again.

> node
Welcome to Node.js v14.17.0.
Type ".help" for more information.
> async function test() {
...   const foo = await import('./foo.js');
...   console.log('foo', foo);
... }
undefined

> test()
Promise { <pending> }
> loading foo
foo [Module: null prototype] { default: 'FOOooOO' }

> test()
Promise { <pending> }
> foo [Module: null prototype] { default: 'FOOooOO' }
npmrun commented 1 year ago

@3cp Do you know how to delete import() cache? I am using this temporary solution:

import("./foo.js?"+new Date().getTime())