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.
- The first argument to
jest.mock()
should be a literal value. - The second argument, if provided, should be an inline function.
- That function shouldn’t reference any variables defined outside the function.
jest.mock()
must be called at the top-level, and not inside another function.- Otherwise, it gets moved to the top of the function it’s called in.
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.