Luke
Luke

Reputation: 19941

How to use ESM tests with jest?

My setup

My goal: Run the ESM tests using jest.

My attempts:

(1) CommonJS App w/ .js test

(2) CommonJS App w/ .mjs test

(3) ESM App w/ .js or .mjs

(4) CommonJS App and CommonJS test

Upvotes: 64

Views: 64982

Answers (9)

Michael Thelin
Michael Thelin

Reputation: 4830

Using ECMAScript modules, Node v20.17.0 and Jest 29.7.0, it was enough to add "type": "module", "test" : "NODE_OPTIONS='--experimental-vm-modules' npx jest" to my package.json.

Upvotes: 1

GVdP
GVdP

Reputation: 165

Since this is the highest voted/rated questions about Jest and ESM, I'll post what I managed to do. Mostly so that I'll find it again when I'll forget all about it.

Tl;Dr: It's.. complicated..

Firstly, read this: https://jestjs.io/docs/ecmascript-modules

My situation

  1. Typescript, but compiling with tsc, and the code below is basically JS
  2. Jest 29.7.0
  3. Node 20.12.2

The code

client.ts

import { simpleFn, simpleFnAsync, functionOfAFunction, functionOfAFunctionAsync } from './library.js';

function simpleFnCaller() {
    console.debug('simpleFnCaller');
    simpleFn({ p1: 10, p2: 20 });
}
async function simpleFnAsyncCaller() {
    console.debug('simpleFnAsyncCaller');
    await simpleFnAsync({ p1: 10, p2: 20 });
}
function functionOfAFunctionCaller() {
    console.debug('functionOfAFunctionCaller');
    const fn = functionOfAFunction({ p1: 10, p2: 20 });
    fn();
}
async function functionOfAFunctionAsyncCaller() {
    console.debug('functionOfAFunctionAsync');
    const fn = functionOfAFunctionAsync({ p1: 10, p2: 20 });
    await fn();
}

async function runAll() {
    simpleFnCaller();

    await simpleFnAsyncCaller();

    functionOfAFunctionCaller();

    await functionOfAFunctionAsyncCaller();
}

export default runAll;

library.ts

export function simpleFn({ p1, p2 }: { p1: number, p2: number }) {
    console.debug(`simpleFn function called. p1: ${p1}, p2: ${p2}`);
};

export async function simpleFnAsync({ p1, p2 }: { p1: number, p2: number }) {
    console.debug(`simpleFnAsync function called. p1: ${p1}, p2: ${p2}`);
};

export function functionOfAFunction({ p1, p2 }: { p1: number, p2: number }) {
    console.debug(`functionOfAFunction function called. p1: ${p1}, p2: ${p2}`);

    return function () {
        console.debug(`return value of functionOfAFunction function called. p1: ${p1}, p2: ${p2}`);
    };
};

export function functionOfAFunctionAsync({ p1, p2 }: { p1: number, p2: number }) {
    console.debug(`functionOfAFunctionAsync function called. p1: ${p1}, p2: ${p2}`);

    return async function () {
        console.debug(`return value of functionOfAFunctionAsync function called. p1: ${p1}, p2: ${p2}`);
    };
};

test.ts

import { jest } from '@jest/globals';

// You need to call this _this way_ and _before_ importing the stuff to mock.
// If you `import`, it will be statically imported and you can kiss your mocks goodbye.
jest.unstable_mockModule('../src/library.js', () => {
    return {
        __esModule: true, // Not sure if it's needed but at one point it threw errors without
        simpleFn:                   jest.fn(({ p1, p2 }: { p1: number, p2: number }) => { }), // For Typescript, the signature must match.
        simpleFnAsync:              jest.fn(({ p1, p2 }: { p1: number, p2: number }) => { }),
        functionOfAFunction:        jest.fn(({ p1, p2 }: { p1: number, p2: number }) => { () => { } }), // The return value matters.
        functionOfAFunctionAsync:   jest.fn(({ p1, p2 }: { p1: number, p2: number }) => { async () => { } }),
    };
});
// Now we dynamically import the dependencies..
const { simpleFn, simpleFnAsync, functionOfAFunction, functionOfAFunctionAsync } = await import('../src/library.js');

// Now we dynamically import the code to test..
const master = await import('../src/client.js');

// If you return a function, and want to test that return value, you need to set it up. Again.
(functionOfAFunction as jest.Mock).mockReturnValue(jest.fn());
(functionOfAFunctionAsync as jest.Mock).mockReturnValue(jest.fn());

describe('Testing master', () => {
    beforeEach(async () => {
        jest.clearAllMocks();
        // Calling the code to test, this way, because reasons.
        master.default();
    });

    it('simpleFnCaller calls simpleFn with correct parameters', () => {
        expect(simpleFn).toHaveBeenCalledWith({ p1: 10, p2: 20 });
    });

    it('simpleFnAsyncCaller calls simpleFnAsync with correct parameters', async () => {
        await new Promise(process.nextTick); // Wait for async function to complete

        expect(simpleFnAsync).toHaveBeenCalledWith({ p1: 10, p2: 20 });
    });

    it('functionOfAFunctionCaller calls functionOfAFunction and the returned function', () => {
        const returnedFn = (functionOfAFunction as jest.Mock).mock.results[0].value;

        expect(functionOfAFunction).toHaveBeenCalledWith({ p1: 10, p2: 20 });
        expect(returnedFn).toHaveBeenCalled();
    });

    it('functionOfAFunctionAsyncCaller calls functionOfAFunctionAsync and the returned function', async () => {
        const returnedFn = (functionOfAFunctionAsync as jest.Mock).mock.results[0].value;

        await new Promise(process.nextTick); // Wait for async function to complete

        expect(functionOfAFunctionAsync).toHaveBeenCalledWith({ p1: 10, p2: 20 });
        expect(returnedFn).toHaveBeenCalled();
    });
});

To run: NODE_OPTIONS="--enable-source-maps --experimental-vm-modules" npx jest -t 'Testing master'

Upvotes: 0

joegomain
joegomain

Reputation: 864

After a lot of fiddling, this is my successful setup (the parts that matter):

  1. package.json has

      "type": "commonjs",
      "scripts": {
        "test": "NODE_OPTIONS=--experimental-vm-modules jest"
      }
    
  2. jest.config.mjs (yes, esModule)

    export default {
      moduleFileExtensions: [
        "mjs",
        // must include "js" to pass validation https://github.com/facebook/jest/issues/12116
        "js",
      ],
      testRegex: `test\.mjs$`,
    };
    

That having to include "js" is a historic relic from the times modules where obviously .js files. Follow https://github.com/facebook/jest/issues/12116 for update.

Upvotes: 13

stamat
stamat

Reputation: 1979

Installing esm npm package

npm i --save-dev esm

did the trick for me without any additional jest configuration. Cause ESM package has a require function that is able to substitute for CJS require. So you'll be overriding the CJS require function with ESM one in every jest file that tests ESM.

So for instance to test yourESModule.js which is an ESM with jest, you would start your yourESModule.test.js with the following:

require = require('esm')(module)
const { yourFunction } = require('./yourESModule')

Btw, my jest version is "jest": "^29.6.1"

Then just run:

npx jest

Hope this helps!

Upvotes: 0

Vladimir Trotsenko
Vladimir Trotsenko

Reputation: 246

For me to work it was only necessary to set script "test" as follows:

"test": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest"

and yeah, don't forget -> npm i cross-env jest

Upvotes: 11

morganney
morganney

Reputation: 13560

Here are the steps I took to run Jest with a test using ESM. The source files under test were also written using ESM.

  1. Set my node version to 14.16.0

  2. Install Jest:

    npm i jest -D
    
  3. Add "type": "module" to package.json

  4. Update test script in package.json:

    "scripts": {
      "test": "node --experimental-vm-modules ./node_modules/.bin/jest"
    }
    
  5. Create a jest.config.js file with the following content:

    export default { transform: {} }
    
  6. Have at least one test that could be found using the default testMatch (mine was inside __tests__ dir)

  7. Run tests:

    npm test
    

There is a more complete example in the magic-comments-loader repository on GitHub.

Upvotes: 57

Kleber Germano
Kleber Germano

Reputation: 770

I followed the @morganney answer but I was getting this error: "basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")...". so to make it work I used the property "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" from the documentation.

Thus my package.json ended up configured like this:

 {
      "type": "module", 
        "jest": {
          "transform": {}
        },
         "scripts": {
            "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
         }
       "dependencies": {
        "jsdom": "^17.0.0",
       }
    }

Upvotes: 2

raquelhortab
raquelhortab

Reputation: 556

in my case, leaving

  • "type": "module" in package.json

and renaming

  • jest.config.js to jest.config.cjs
  • babel.config.js to babel.config.cjs

worked

Upvotes: 4

Brandon Aaskov
Brandon Aaskov

Reputation: 352

Here's the minimum you need. Props to @Luke for the comment above with the testMatch answer I needed.

  1. package.json - Make sure "type": "module" is in there

  2. jest.config.js - Add this glob to your testMatch array: "**/?(*.)+(spec|test).(m)js"

  3. Make sure your test file(s) end in .spec.mjs or .test.mjs to match the glob

  4. Set your npm test script to node --experimental-vm-modules ./node_modules/.bin/jest

That's it. You do not need the transform setting in the jest config as the documentation suggests.

Upvotes: 3

Related Questions