Skip to content

Mocking Node Modules

Testing code that touches the file system, spawns processes, or makes network calls requires mocking Node.js's built-in modules. This lesson covers practical patterns for fs, path, and similar core modules.

Mocking the fs Module

The fs module is one of the most commonly mocked Node.js core modules, since tests should never depend on real files existing on disk. jest.mock('fs') combined with a factory (or a manual mock) lets you simulate reads and writes entirely in memory.

This keeps tests fast and portable across machines and CI environments, where the exact file system layout can't be guaranteed.

// configLoader.js
const fs = require('fs');
function loadConfig(path) {
  return JSON.parse(fs.readFileSync(path, 'utf8'));
}
module.exports = { loadConfig };

// configLoader.test.js
jest.mock('fs');
const fs = require('fs');
const { loadConfig } = require('./configLoader');

test('parses JSON config from a file', () => {
  fs.readFileSync.mockReturnValue('{"env":"test"}');

  expect(loadConfig('config.json')).toEqual({ env: 'test' });
  expect(fs.readFileSync).toHaveBeenCalledWith('config.json', 'utf8');
});

No real file is read; fs.readFileSync is entirely controlled by the test.

Mocking child_process for Command Execution

jest.mock('child_process');
const { execSync } = require('child_process');

execSync.mockReturnValue(Buffer.from('v18.0.0'));
  • Mock child_process to avoid actually spawning shell commands in tests.
  • Return values from execSync/exec should match their real Buffer/string/callback shapes.
  • For callback-based APIs (exec), mock the implementation to invoke the callback manually.
  • Never let a test genuinely execute shell commands — it's slow, unpredictable, and unsafe in CI.

Node Core Module Mocking Cheat Sheet

Common core modules you'll need to mock and why.

Module Why Mock It
fs Avoid touching the real file system
path Rarely mocked; usually safe to use directly
child_process Avoid spawning real shell commands
os Simulate specific platforms or environment details
crypto Make randomness/hashing deterministic in tests

Mocking Callback-Based Node APIs

Some Node.js APIs use callbacks instead of return values or promises. To mock these, use .mockImplementation() to manually invoke the callback argument with your desired fake result.

jest.mock('fs');
const fs = require('fs');

fs.readFile.mockImplementation((path, encoding, callback) => {
  callback(null, '{"env":"test"}');
});

The mock implementation manually calls the callback, simulating an asynchronous, successful read.

Preferring fs.promises for Easier Mocking

When possible, use the promise-based fs.promises API (or fs/promises) in your source code instead of callback-based functions — mocking promise-returning functions with .mockResolvedValue() is simpler and more readable than manually invoking callbacks.

jest.mock('fs/promises');
const fs = require('fs/promises');

fs.readFile.mockResolvedValue('{"env":"test"}');

Common Mistakes

  • Letting a test accidentally read or write a real file because fs wasn't mocked.
  • Forgetting callback-based Node APIs need .mockImplementation() to manually call the callback.
  • Mocking path unnecessarily when its real, pure behavior is usually safe to test against directly.
  • Not mocking child_process and accidentally running real shell commands during CI test runs.

Key Takeaways

  • Mock Node.js core modules like fs and child_process to keep tests isolated from the real system.
  • Callback-based APIs require manually invoking the callback inside .mockImplementation().
  • Promise-based equivalents (fs/promises) are easier to mock with .mockResolvedValue().
  • Never let tests perform real file system or process operations.

Pro Tip

When your source code has a choice, prefer promise-based Node.js APIs (fs/promises) over callback-based ones — not just for cleaner application code, but because they are noticeably easier to mock in tests.