Jest cannot find dynamicly loaded ES6 module - node.js

The ipfs-core library is only available as ES6 Module, while the project uses CommonJS. Therefore I use a dynamic import.
If your application is not yet ESM or you are not ready to port it to ESM, you can use the dynamic import function to load ipfs at runtime from a CJS module.
See Migrating to ipfs#0.63 and ipfs-core#0.15
export const ipfsProviders = [
{
provide: 'IPFS',
useFactory: async (): Promise<any> => {
const IPFS = await import('ipfs-core');
return await IPFS.create({ start: false });
}
},
];
The application runs fine, but I'm unable to run tests with Jest for services where IPFS is a dependency.
Error: Cannot find module 'ipfs-core' from 'common/ipfs/ipfs.providers.ts'
How can I configure Jest, so it just treats ipfs-core as ES module, while seeing the rest of the code as CommonJs?

Related

How to mock electron when running jest with #kayahr/jest-electron-runner

Setup
I want to unit test my electron app with jest. For this I have the following setup:
I use jest and use #kayahr/jest-electron-runner to run it with electron instead of node. Additionally, since it is a typescript project, I use ts-jest.
My jest.config.js looks like this:
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageProvider: 'v8',
preset: 'ts-jest',
runner: '#kayahr/jest-electron-runner/main',
testEnvironment: 'node',
};
The test is expected to run in the main process. I have reduced my code to the following example function:
import { app } from 'electron';
export function bar() {
console.log('in bar', app); //this is undefined when mocked, but I have a real module if not mocked
const baz = app.getAppPath();
return baz;
}
The test file:
import electron1 from 'electron';
import { bar } from '../src/main/foo';
console.log('in test', electron1); //this is undefined in the test file after import
// jest.mock('electron1'); -> this does just nothing, still undefined
const electron = require('electron');
console.log('in test after require', electron); //I have something here yay
jest.mock('electron'); //-> but if I comment this in -> it is {} but no mock at all
it('should mock app', () => {
bar();
expect(electron.app).toBeCalled();
});
What do I want to do?
I want to mock electron.app with jest to look whether it was called or not.
What is the problem?
Mocking electron does not work. In contrast to other modules like fs-extra where jest.mock() behaves as expected.
I don't understand the behavior happening here:
Importing "electron" via import in the file containing the tests (not the file to be tested!) does not work (other modules work well), but require("electron") does.
I do have the electron module if not mocked in bar(), but after mocking not
while jest.mock("fs-extra") works, after jest.mock("electron") electron is only an empty object, not a mock
I would really like to understand what I did wrong or what the problem is. Switching back to #jest-runner/electron does not seem to be an option, since it is not maintained anymore. Also I don't know if this is even the root of the problem.
Has anyone seen this behavior before and can give me a hint where to start searching?

Jest error "Cannot use import statement outside a module" when importing node-fetch even with the CommonJS format

I'm pretty new to node.js and I'm confused with the import/export system. If I install a package using NPM in my project's node_modules directory, should I eye-check it to know whether it has used the ES6 module system or the CommonJS module system to export its things, and then use the same system for my imports accordingly?!
Node's documentation says it's interoperable in imports:
An import statement can reference an ES module or a CommonJS module.
However, it doesn't seem to work in my case. My problem is, I have set "module": "commonjs", in my tsconfig.json file and so the compiled output will have commonJS imports, however, in a typescript test file I have imported node-fetch like this: import fetch from 'node-fetch', then when I compile it (tsc) and run jest on the files in the build directory it gives this error:
SyntaxError: Cannot use import statement outside a module
16 | const supertest_1 = importDefault(require("supertest"));
---> 17 | const node_fetch_1 = importDefault(require("node-fetch"));
When I search the above error on StackOverflow the existing answers say "jest does not support ES6 modules completely yet (the support is experimental)", however, the point is, I am not using ES6 module imports in this case at all!. As I explained, the compiled files will have commonJS imports... (and jest is running those compiled tests too).
Here are some parts of my code that might be relevant to this question:
// jest.config.js
const { defaults } = require('jest-config');
/** #type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
testMatch: ["**/dist/test/**/*", ...defaults.testMatch],
};
// test/example-test.ts
import app from '../src/app';
import request from "supertest";
import fetch from 'node-fetch';
describe(" ..... ", () => { //...
Is it a problem of jest? Or a problem of node-fetch? Or even maybe the imports in the compiled output of TypeScipt?
Also here's the compiled import:
// dist/test/example-test.js
//...
const app_1 = __importDefault(require("../src/app"));
const supertest_1 = __importDefault(require("supertest"));
const node_fetch_1 = __importDefault(require("node-fetch"));
Oh wow figured it out, for future visitors...
Don't include a jest.config.js and add a babel.config.js with:
module.exports = {
presets: [['#babel/preset-env']],
};
makes sure to have
"#babel/core": "^7.18.9",
"#babel/preset-env": "^7.18.9",
"babel-jest": "^28.1.3",
in package.json

Dynamic import in NodeJS (in Gatsby): A dynamic import callback was not specified

I'm trying to use the dynamic imports in NodeJS, with a eS6 file, but can't get it work
I'm using it in a gatsby project within its gatsby-node.js file
exports.createPages = async props => {
//...
const name = './test'
;(async () => {
const data = await import(name)
console.log(data)
})()
Where test.js is just
export const hey = 'hi'
But I always get this A dynamic import callback was not specified.
Why is it not working? NodeJS version is 12.18.4
Update (11th of November 2022)
The syntax should be valid without importing any additional plugins with Babel.
If you need them or need to tweak/customize its configuration, please continue reading.
Since it's not currently a publish version, you need to install each desired module (babel-plugin-syntax-dynamic-import in your case) of ES6 and add it yo your babel configuration.
Run:
npm install --save-dev #babel/plugin-syntax-dynamic-import
Then, in your Babel file (babel.config.js or .babelrc in the root of your project) add it to plugins array:
"plugins": ["#babel/plugin-syntax-dynamic-import"]
Ideally, your Babel file should look like:
module.exports = function(api) {
api.cache(true);
const presets = [
[`#babel/preset-env`, { 'useBuiltIns': `usage`, 'corejs': `2` }],
[`#babel/preset-react`, { 'development': true, minify: true }],
];
const plugins = [
`#babel/plugin-syntax-dynamic-import`,
];
return { presets, plugins };
};

How to use TypeScript in a Custom Test Environment file in Jest?

I need to enable some global variables to be reachable for my test so I am setting up a Custom Environment to be used in the testEnvironment option in my jest.config.json to achieve that.
For our project we have a TypeScript file that we use for setupFilesAfterEnv option and that works just fine, however the testEnvironment seems to support only ES5. Is there any way to use TypeScript in such option?
I successfully created a Custom Jest Environment using ES5 syntax, however since we are injecting global variables I need TypeScript to also declare a global namespace see: https://stackoverflow.com/a/42304473/4655076.
{
///...
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'], // This works with ts
testEnvironment: '<rootDir>/test/customJestEnvironment.ts', // This doesn't work with ts
}
You might find this helpful: Configure Jest global tests setup with .ts file (TypeScript)
But basically you can only pass in compiled JS files as environments.
You can do what that article suggests. But it didn't work for me out of the box. So I manually compile my env.
i.e.
in package.json
"test": "tsc --lib es6 --target es6 --skipLibCheck -m commonjs --esModuleInterop true path/to/env.ts &&
jest --config=jest.config.js",
And in jest.config.js
{
testEnvironment: '<rootDir>/path/to/env.js', // Note JS extension.
}
I solved this by using ts-node and the following command:
node -r ts-node/register ./node_modules/jest/bin/jest.js
This essentially compiles the typescript on-the-fly, so that jest receives the emitted javascript, without the need of actually compiling your typescript sources to js.
You will need to enable esModuleInterop TS compiler option for this to work properly.
TestEnvironment.ts
import NodeEnvironment from 'jest-environment-node';
import type {Config} from '#jest/types';
class TestEnvironment extends NodeEnvironment {
constructor(config: Config.ProjectConfig) {
super(config);
// this.testPath = context.testPath;
// this.docblockPragmas = context.docblockPragmas;
}
public async setup(): Promise<void> {
await super.setup();
console.log('SETTING UP...');
// await someSetupTasks(this.testPath);
// this.global.someGlobalObject = createGlobalObject();
// // Will trigger if docblock contains #my-custom-pragma my-pragma-value
// if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') {
// // ...
// }
}
public async teardown(): Promise<void> {
await super.teardown();
console.log('TEARING DOWN!');
// this.global.someGlobalObject = destroyGlobalObject();
// await someTeardownTasks();
}
}
export default TestEnvironment;
This solution however, will break globalSetup -- if you use jest-ts.
As you might know, typescript files are just superset to javascript to provide strong type checking. Jest's engine/runtime however expects your files in CommonJS format javascript files.
You can have a separate tsconfig.env.json just for this env.ts. Compile this before running jest test and use the compiled env.js in your jest.config.js.
tsc -p tsconfig.env.json && jest
Also i have never seen people writing configuration files in TS.
why CommonJS ? because jest is essentially running on top of node. node supports Javascript files in CommonJS format. Node has started supporting es modules as well recently! This is a big thing!
You can create a global.d.ts file at the root of your project.
Then you can define global variables as seen below. In my case, it was a NestJS application, but you can define anything.
declare global {
namespace NodeJS {
interface Global {
app: INestApplication;
}
}
}
This is another example for client project where we define window properties like innerWidth;
declare namespace NodeJS {
interface Global {
innerWidth: number;
dispatchEvent: Function;
}
}
Inside your .d.ts definition file:
type MyGlobalFunctionType = (name: string) => void
Add members to the browser's window context:
interface Window {
myGlobalFunction: MyGlobalFunctionType
}
Same for NodeJS:
declare module NodeJS {
interface Global {
myGlobalFunction: MyGlobalFunctionType
}
}
Now you declare the root variable
declare const myGlobalFunction: MyGlobalFunctionType;
Then in a regular .ts file, but imported as side-effect, you actually implement it:
global/* or window */.myGlobalFunction = function (name: string) {
console.log("Hey !", name);
};
And finally use it elsewhere :
global/* or window */.myGlobalFunction("Ayush");
myGlobalFunction("Ayush");

Stub an export from a native ES Module without babel

I'm using AVA + sinon to build my unit test. Since I need ES6 modules and I don't like babel, I'm using mjs files all over my project, including the test files. I use "--experimental-modules" argument to start my project and I use "esm" package in the test. The following is my ava config and the test code.
"ava": {
"require": [
"esm"
],
"babel": false,
"extensions": [
"mjs"
]
},
// test.mjs
import test from 'ava';
import sinon from 'sinon';
import { receiver } from '../src/receiver';
import * as factory from '../src/factory';
test('pipeline get called', async t => {
const stub_factory = sinon.stub(factory, 'backbone_factory');
t.pass();
});
But I get the error message:
TypeError {
message: 'ES Modules cannot be stubbed',
}
How can I stub an ES6 module without babel?
According to John-David Dalton, the creator of the esm package, it is only possible to mutate the namespaces of *.js files - *.mjs files are locked down.
That means Sinon (and all other software) is not able to stub these modules - exactly as the error message points out. There are two ways to fix the issue here:
Just rename the files' extension to .js to make the exports mutable. This is the least invasive, as the mutableNamespace option is on by default for esm. This only applies when you use the esm loader, of course.
Use a dedicated module loader that proxies all the imports and replaces them with one of your liking.
The tech stack agnostic terminology for option 2 is a link seam - essentially replacing Node's default module loader. Usually one could use Quibble, ESMock, proxyquire or rewire, meaning the test above would look something like this when using Proxyquire:
// assuming that `receiver` uses `factory` internally
// comment out the import - we'll use proxyquire
// import * as factory from '../src/factory';
// import { receiver } from '../src/receiver';
const factory = { backbone_factory: sinon.stub() };
const receiver = proxyquire('../src/receiver', { './factory' : factory });
Modifying the proxyquire example to use Quibble or ESMock (both supports ESM natively) should be trivial.
Sinon needs to evolve with the times or be left behind (ESM is becoming defacto now with Node 12) as it is turning out to be a giant pain to use due to its many limitations.
This article provides a workaround (actually 4, but I only found 1 to be acceptable). In my case, I was exporting functions from a module directly and getting this error: ES Modules cannot be stubbed
export function abc() {
}
The solution was to put the functions into a class and export that instead:
export class Utils {
abc() {
}
}
notice that the function keyword is removed in the method syntax.
Happy Coding - hope Sinon makes it in the long run, but it's not looking good given its excessive rigidity.
Sticking with the questions Headline „Stub an export from a native ES Module without babel“ here's my take, using mocha and esmock:
(credits: certainly #oligofren brought me on the right path…)
package.json:
"scripts": {
...
"test": "mocha --loader=esmock",
"devDependencies": {
"esmock": "^2.1.0",
"mocha": "^10.2.0",
TestDad.js (a class)
import { sonBar } from './testSon.js'
export default class TestDad {
constructor() {
console.log(purple('constructing TestDad, calling...'))
sonBar()
}
}
testSon.js (a 'util' library)
export const sonFoo = () => {
console.log(`Original Son 'foo' and here, my brother... `)
sonBar()
}
export const sonBar = () => {
console.log(`Original Son bar`)
}
export default { sonFoo, sonBar }
esmockTest.js
import esmock from 'esmock'
describe.only(autoSuiteName(import.meta.url),
() => {
it('Test 1', async() => {
const TestDad = await esmock('../src/commands/TestDad.js', {
'../src/commands/testSon.js': {
sonBar: () => { console.log('STEPSON Bar') }
}
})
// eslint-disable-next-line no-new
new TestDad()
})
it('Test 2', async() => {
const testSon = await esmock('../src/commands/testSon.js')
testSon.sonBar = () => { console.log('ANOTHER STEPSON Bar') }
testSon.sonFoo() // still original
testSon.sonBar() // different now
})
})
autoSuiteName(import.meta.url)
regarding Test1
working nicely, import bended as desired.
regarding Test1
Bending a single function to do something else is not a problem.
(but then there is not much test value in calling your very own function you just defined, is there?)
Enclosed function calls within the module (i.e. from sonFoo to sonBar) remain what they are, they are indeed a closure, still pointing to the prior function
Btw also tested that: No better results with sinon.callsFake() (would have been surprising if there was…)

Resources