Doug.Instance

Be Careful Mocking path

Aug 21, 2023

TL/DR;

If you mock path.resolve() in a jest test, it could break any require() or import calls downstream. Best to wrap all calls in a helper method and mock the wrapper instead of path.resolve() so that you don't break anything that actually needs a local path.

The Problem

Recently, I ran into an issue that was fairly challenging to troubleshoot. I was running tests in jest and received the following error:

Cannot find module 'jest-util'
    Require stack:
    - /code/codetender/node_modules/@jest/console/build/CustomConsole.js
    - /code/codetender/node_modules/@jest/console/build/index.js
    - /code/codetender/node_modules/@jest/core/build/cli/index.js
    - /code/codetender/node_modules/@jest/core/build/index.js
    - /code/codetender/node_modules/jest-cli/build/run.js
    - /code/codetender/node_modules/jest-cli/build/index.js
    - /code/codetender/node_modules/jest-cli/bin/jest.js
    - /code/codetender/node_modules/jest/bin/jest.js

The Solution

After a quick google, none of the posted solutions seemed to be related to my problem. It only seemed to be happening with certain test suites which were unremarkably different from any of the others that were working. So in order to find the problem, I turned to the tried-and-true debugging method:

  1. Start from scratch.
  2. Add things back one line at a time until things break.

I'm testing a codetender which is a CLI that manipulates the file system. The only thing the constructor of the main class does is parse the configuration passed to the constructor. The only real logic in that parsing is that the resolution of relative paths to local paths happens right away using path.resolve(). Since I use path.resolve() in many other places as well, instead of using mockReturnValueOnce(), I was using mockImplementation() to create a dummy root path I could test against like this:

      jest.spyOn(path, 'resolve').mockImplementation((path) => `/path/and/${path}`);

The issue was, any time a module that called require (or import in TypeScript) to a local module was referenced in my tests, Node attempted to resolve the relative path of the module to the local path using path.resolve(). Since this was mocked, it could not find the module and raised the above exception.

Since I already had a helper class to deal with file system interaction, I simply added a static resolve method to this class so I could mock that class instead of path.resolve() and then mocking path.resolve() in only the unit test for the static function. I did switch all of my mocks to use mockReturnValueOnce() just to be make the test more discreet and easier to read, but using the wrapper class ensured that there would not be a require() call in between the mock and the implementation.