Disclaimer: This article has nothing to do with C++. Also, I do not have a huge amount of experience with the Node.js ecosystem. This information is provided here because of the issues I had reading outdated, and often incorrect, “solutions” on forums and blogs. I hope someone finds it useful.
Suppose you have a database.js
module which interacts with a database server to query and update users. This is abstracted away from the client user.js
module, which is what your Node.js/Express app communicates with. Now suppose you want to write unit tests for user.js
without accessing the real database. You could create a “testing” database, but a better way may be to “mock” database.js
. This is where the testing framework imports user.js
, while providing its own functions which override those in database.js
for the duration of the test suite.
I’m not going to try to describe all of Jest here, and I’m assuming also that the reader is familiar with ESM and the import
keyword together with both named and default export
s. The method which works for me is as follows:
- Create a test file
user.test.js
which imports the Jest framework - Import the module you want to mock (
database.js
) dynamically withawait import()
- Redefine the exports as desired within
jest.unstable_mockModule()
- Import the module you want to unit-test (
user.js
), again dynamically - Write the tests with
test('what it tests', async () => { test here... });
- Create a suitable
package.json
andjest.config.json
- Set environment variable (important – still necessary with v21.6):
NODE_OPTIONS=--experimental-vm-modules
- Run
npm test
in the project directory
Here is the sample user.test.js
demonstrating the above:
import { jest } from '@jest/globals';const mockModule = await import('./database.js');jest.unstable_mockModule('./database.js', () => ({ ...mockModule, default: jest.fn(() => 1), fetchFromDatabase: jest.fn().mockResolvedValue({ id: 42, name: 'John Doe', email: '[email protected]' }),}));const { getUser, makeUser, getTotalUsers } = await import('./user.js');test('getUser() returns correct name and email', async () => { const user = await getUser(1); expect(user.user).toBe('John Doe'); expect(user.email).toBe('[email protected]');});test('getTotalUsers() returns correct number', async () => { const numberOfUsers = getTotalUsers(); expect(numberOfUsers).toBe(1);});test('makeUser() is callable', async() => { const newUser = await makeUser('Jane Doe', '[email protected]'); expect(newUser).toBe(undefined);});
Line 6 is the spread operator which imports all of the exports of database.js
into the mock. Line 7 redefines the default export getNumberOfUsers()
to always return 1
. Line 8 defines the result of fetchFromDatabase()
(which is a Promise as it is an async function) to always be resolved as a dummy user. (Should mocking all the exports be required, lines 3 and 6 are not needed.)
Here is user.js
:
import getNumberOfUsers, { fetchFromDatabase, generateUserId } from './database.js';export async function getUser(userId) { const user = await fetchFromDatabase(userId); return { user: user.name, email: user.email };}export async function makeUser(user, email) { const newUser = { id: generateUserId(), user: user, email: email }; // store this somehow return newUser;}export function getTotalUsers() { return getNumberOfUsers();}
And here is the dummy database.js
:
const db_credentials = { username: 'dbuser', password: '$$hashed_password$$' };// We want to mock this functionexport async function fetchFromDatabase(id) { // Query database here return [{ id: 1, name: 'Alice', email: '[email protected]' }, { id: 2, name: 'Bob', email: '[email protected]' }, { id: 3, name: 'Charlie', email: '[email protected]' }][id];}// We don't want to mock this functionexport function generateUserId() { const date = new Date(); return date.getTime();}// This is the default export, which we do want to mockfunction getNumberOfUsers() { // Query database here return 3;}export default getNumberOfUsers;
Finally a minimalist jest.config.json
which prevents harmful rewriting of our test module:
{ "transform": {}}
And a package.json
setting the necessary options:
{ "type": "module", "scripts": { "test": "jest" }}
Now you can run (Windows):
set NODE_OPTIONS=--experimental-vm-modulesnpm test
Or (everything else):
NODE_OPTIONS=--experimental-vm-modules npm test
The output should hopefully look something like:
Finally, the source is available to download from here (as a .zip), and the version of Jest used was 29.7.0. (Node.js latest stable v21.6.1 and npm 10.2.4). This process is likely to change, so if you are writing tests for a larger project and don’t want them to break in the future, you may wish to install Jest as a development dependency with npm install --save-dev jest
, which will install Jest in a node_modules
folder within your project.
If you have any issues or additions to make to the above, please leave a comment; I shall try to keep this article updated as support for ESM with Jest matures.