// my-func.test.js
import { jest } from '@jest/globals';
import { myFunc } from './my-func.js';
import fs from 'node:fs';
const mock = jest.fn().mockReturnValue({isDirectory: () => true});
jest.spyOn(fs, 'statSync').mockImplementation(mock);
it('should work', () => {
expect(myFunc('/test')).toBeTruthy();
expect(mock).toHaveBeenCalled();
});
The test passes with this implementation:
// my-func.js
import fs from 'node:fs';
// import { statSync } from 'node:fs';
export function myFunc(dir) {
const stats = fs.statSync(dir, { throwIfNoEntry: false });
// const stats = statSync(dir, { throwIfNoEntry: false });
return stats.isDirectory();
}
However, if the implementation is changed to (note the different importing mechanism):
// my-func.js
// import fs from 'node:fs';
import { statSync } from 'node:fs';
export function myFunc(dir) {
// const stats = fs.statSync(dir, { throwIfNoEntry: false });
const stats = statSync(dir, { throwIfNoEntry: false });
return stats.isDirectory();
}
Jest will fail with:
Cannot read properties of undefined (reading 'isDirectory')
TypeError: Cannot read properties of undefined (reading 'isDirectory')
How do I spy on or mock statSync
using the import { statSync } from 'node:fs';
syntax? Is it even possible?
Note that the test is being run in the ES module mode:
# package.json
{
"type": "module",
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
...
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@jest/globals": "^29.7.0",
"globals": "^15.14.0",
"jest": "^29.7.0",
...
},
...
}
2
Answers
Jest works by replacing or spying on properties of objects (like fs.statSync when fs is imported as a whole). The issue with a named import is that there’s no object or property available to mock
Your updated implementation fails because the code under test is no longer using the
fs
module object your test is spying on – the named import brings in the relevant function directly. However this tells you a few things:statSync
returnsundefined
, surely the function should returnfalse
(or at least throw a more informative error)?Using
jest.spyOn(fs, 'statSync')
means your test is coupled to the implementation detail that the code it’s testing usesimport fs from 'node:fs'
. To successfully mock the wholefs
module instead, you need to refer to Module mocking in ESM. Applying this guidance to your specific case:Note that it’s not always the case that the default export is an object containing all of the named exports; as always, it’s important to ensure that the test double has the same interface as the thing it’s replacing.
This test will now pass with either implementation (using named or default export to access
statSync
).However, given what you’re trying to test, another way to test it (that depends on even fewer implementation details – you can now use methods other than
statSync
if desired) is to actually try it on the local file system. For example, assuming it’s in the root of a conventional npm project (alternatively you could use a test fixtures directory with a known setup):As noted above, this shows that there’s one case where the behaviour probably isn’t correct: