When I first started working with our existing tests in Microsoft Loop ’s codebase, I saw this require pattern that confused me.
I’ve never had to switch to require statements before when using Jest, so I was surprised to see this. Normally I would just import the module directly.
Using import statements is much more succinct. So why was a different pattern used? I left it alone when I first joined, but now I’ve done some research and software archaeology to better understand where this pattern arose, when its needed, and when it can be removed.
This pattern is only useful when using
jest.mock
The first thing that threw me off is the name
getMocksForTest
. There’s no mocking involved, so the name didn’t seem appropriate. But I later found test files with a more complicated setup.
Here, we use
jest.mock()
to mock a module in the file
../pages.ts
. The module we’re testing,
./module-to-test.ts
, depends on
../pages.ts
and imports it.
If we had imported the module to test prior to running
jest.mock()
, then our mock would not have been set up in time. Instead, the original version of
../pages.ts
would have been imported.
Now that I’ve encountered a function that actually creates mocks, the name makes more sense. Later on, developers would copy-paste tests and just remove what they didn’t need. That’s how we ended up with functions with the name “get mocks” that never did any mocking.
But
if a test file never uses
jest.mock()
, then this pattern is totally unnecessary
. It can be cleaned up and the module can be imported directly instead.
Isn’t
jest.mock()
always run after import statements?
Now we know that this pattern is needed when
jest.mock()
runs after the module is imported. But lots of test files do use
jest.mock()
and I’ve never seen this require pattern used before. So how does
jest.mock()
normally work?
It turns out Jest runs some special transformations on your test files before running them. It doesn’t just transform TypeScript to JavaScript, it also hoists (aka moves)
jest.mock()
calls to the top of the file
, before any import statements. This is done using the
babel-jest
plugin
, which is enabled by default.
As a result, a test file like this:
will get transformed into a Node.js-compatible file like below:
Does
jest.mock
always get hoisted?
Nope! There are particular conditions necessary for a module mock to be hoisted. Here’s the checks I found from digging through the source code of Jest’s babel plugin .
jest.mock()
should be a literal value.
jest.mock()
must be called at the top-level, and not inside another function.
Essentially, if you can’t just cut-and-paste your
jest.mock()
code to the top of the file, it won’t get automatically moved. Referring to other variables defined earlier will stop the function from being hoisted.
If you do need to refer to variables defined outside the mock function, you can either import them separately or use
jest.doMock
to explicitly avoid hoisting.
As long as all your module mocks can be hoisted, it’s safe to import your code to test using an import statement .