Filesystem mocking with unionfs and Jest
Mocking Node’s fs module can get tricky.
Especially if you want to mix real filesystem access with in-memory mocks. A
common approach with memfs and unionfs makes this possible, but it isn’t
always obvious1 how to patch the filesystem with Jest mocking.
Here’s a working example:
import { vol } from 'memfs';
import { ufs } from 'unionfs';
import fs from 'fs';
import path from 'path';
// remember to mock both fs and fs/promises + node: prefixed imports
jest.mock('fs', () => jest.requireActual('unionfs').ufs);
jest.mock('fs/promises', () => jest.requireActual('unionfs').ufs.promises);
jest.mock('node:fs', () => jest.requireActual('unionfs').ufs);
jest.mock('node:fs/promises', () => jest.requireActual('unionfs').ufs.promises);
// add your mock files here
vol.fromJSON({
'/foo/bar/baz.txt': 'hello from mock',
});
// combine real fs and in-memory fs
ufs.use(jest.requireActual('fs')).use(vol);
it('unionfs', async () => {
// mocked file works
expect(fs.readFileSync('/foo/bar/baz.txt', 'utf8')).toBe('hello from mock');
// fs/promises also works
await expect(fs.promises.readFile('/foo/bar/baz.txt', 'utf8')).resolves.toBe('hello from mock');
// real files work too
// - this is useful for preserving logic that reads required system files that a dependency might assume exists.
// - you can also *override* the value of this file in `vol.fromJSON` above.
expect(fs.readFileSync('/etc/os-release', 'utf8')).toContain('NAME');
});
Why this matters
- If you only mock
fs, you lose access to your actual project files. - If you only use
memfs, you can’t test real paths.
By combining them with unionfs, you get the best of both worlds: real files where you need them, mocked files where you don’t.