Mock a node module using jest with relative paths react - jestjs

I have installed a node module called #SomeOrg/SomeLibrary. It is being imported in source code in multiple ways in multiple files. For example,
import { SOME_IMPORTS_FROM_MODULE1} from "#SomeOrg/SomeLibrary/Module1"
import {SOME_IMPORTS_FROM_SUB_MODULE} from "#SomeOrg/SomeLibrary/Module1/SubModule"
import {SOME_IMPORTS_FROM_MODULE2} from "#SomeOrg/SomeLibrary/Module2"
etc.
How can I mock the #SomeOrg/SomeLibrary using JEST so that it would work correctly on each file's test cases? Actually, I have tried the following way to mock it in a file and added it to the setupFiles key in the JEST config.
jest.mock("#SomeOrg/SomeLibrary", () => ({
Module1: {
SOME_IMPORTS_FROM_MODULE1: jest.fn(),
SubModule: {
SOME_IMPORTS_FROM_SUB_MODULE: jest.fn()
}
},
Module2: {
SOME_IMPORTS_FROM_MODULE2: jest.fn()
}
))
But it is not working. Please suggest me a generic way so that it would work for all relative paths imported from a node module.

Here is the working solution for the above scenario. But this is not an optimal one. Edits are welcome.
1. module1_mock.js
jest.mock('#SomeOrg/SomeLibrary/Module1', () => {
return {
SOME_IMPORTS_FROM_MODULE1: jest.fn()
}
})
2. sub_module_mock.js
jest.mock('#SomeOrg/SomeLibrary/Module1/SubModule', () => {
return {
SOME_IMPORTS_FROM_SUB_MODULE: jest.fn()
}
})
3. module2_mock.js
jest.mock('#SomeOrg/SomeLibrary/Module2', () => {
return {
SOME_IMPORTS_FROM_MODULE2: jest.fn()
}
})
4. module_mock.js
jest.mock('#SomeOrg/SomeLibrary', () => {
return {
__esModule: true,
default: jest.fn(),
}
})
Once the above files are created, add them to the setupFiles key inside the jest configuration file like below.
{
...defaultConfig,
setupFiles: ["DIRECTORY_OF_MOCKS/module1_mock.js","DIRECTORY_OF_MOCKS/sub_module_mock.js","DIRECTORY_OF_MOCKS/module2_mock.js","DIRECTORY_OF_MOCKS/module_mock.js"]
}

Related

Jest Spy not being called

I am trying to run a test using the winston logger package. I want to spy on the createlogger function and assert that it is being called with the correct argument.
Logger.test.ts
import { describe, expect, it, jest, beforeEach, afterEach } from '#jest/globals';
import { LogLevel } from 'api-specifications';
import winston, { format } from 'winston';
import { buildLogger } from './Logger';
import { LoggerConfig } from './Config';
describe('Logger', () => {
beforeEach(() => {
jest.spyOn(winston, 'createLogger');
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should call winston createLogger with format.json when config.json is true', () => {
const config: LoggerConfig = {
json: true,
logLevel: LogLevel.INFO,
};
buildLogger(config);
expect(winston.createLogger).toHaveBeenCalledWith(
expect.objectContaining({
level: LogLevel.INFO,
format: format.json(),
}),
);
});
});
Logger.ts
import { createLogger, format, transports, Logger } from 'winston';
import { LoggerConfig } from './Config';
const logFormatter = format(info => {
const values = (info[Symbol.for('splat') as any as string] ?? [])
.filter(f => typeof f === 'object')
.reduce(
(acc, curr) => ({
...acc,
...curr,
}),
{},
);
const meta = Object.keys(values)
.map(k => ` - ${k}=${values[k]}`)
.join('');
return { ...info, [Symbol.for('message')]: `${info.level}: ${info.message}${meta}` };
});
export const buildLogger = (config: LoggerConfig): Logger =>
createLogger({
level: config.logLevel,
format: config.json ? format.json() : logFormatter(),
transports: [new transports.Console()],
});
However when i run the test i get the following output
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: ObjectContaining {"format": {"options": {}}, "level": "info"}
Number of calls: 0
Im not quite sure what going on.
Im using the following versions of packages:
"jest": "28.1.0"
"ts-jest": "28.0.2"
Assuming you are using ES modules, there are many ways to solve this issue. Honestly, I do not know which one is better (all of them have upsides and downsides), there might even be a well-known solution that I have not found yet, but I doubt it. The reason is that, from what I have read, Jest support for ES modules is still incomplete, as the documentation points out:
Please note that we currently don't support jest.mock in a clean way in ESM, but that is something we intend to add proper support for in the future. Follow this issue for updates.
So, all the followings are just workarounds, not real solutions.
#1 - Always import the default object
You can import winston in 2 ways:
import * as winston from 'winston': this notation returns a Module object, containing the exported properties. Among them you can find a default property, pointing to module.exports of the CommonJS module.
import winston from 'winston': this is a syntactic sugar for import { default as winston } from 'winston'. Basically, instead of importing the entire module, you just get the default property.
You can read more about it here.
createLogger can be accessed in 2 ways if you use the first import notation:
[Module] object
{
...
createLogger: f() { ... }
default: {
...
createLogger: f() { ... }
}
}
I am not sure mocking a Module object is possible, but in your case it is enough to mock default.createLogger. This is quite easy:
Logger.ts
import winston from 'winston'
export const buildLogger = async (config) => {
return winston.createLogger({
level: "info"
});
}
(Logger.test.ts is the original one.)
Why does this work? Because both Logger.test.ts and Logger.ts assign to winston (a reference to) the default object. jest.spyOn(winston, 'createLogger') creates a spy on the method default.createLogger, because we have imported only the default object. Therefore, the mocked implementation gets shared with Logger.ts as well.
The downside is that an import statement like import { createLogger } from 'winston' cannot work because you are accessing Module.createLogger instead of Module.default.createLogger.
#2 - First mock, then import
With ES modules, import statements are hoisted: even if the first line of your Logger.test.ts was jest.mock('winston', ...), the Logger module would be loaded before that line (because of import { buildLogger } from './Logger';). This means that, in the current state, Logger.ts references the actual implementation of createLogger:
Jest loads Logger.test.ts
Node module loader loads all the modules imported with import ... from ...
Logger.ts is executed, preceded by with import { createLogger } from 'winston'.
Node continues to execute Logger.test.ts, Jest creates a spy on createLogger, but Logger.ts already references the actual implementation of that method.
To avoid the hoisting, a possibility is to use dynamic imports:
Logger.test.ts
import { jest } from '#jest/globals';
jest.mock('winston', () => {
return {
// __esModule: true,
// default: () => "test",
createLogger: jest.fn()
}
});
const winston = await import('winston')
const { buildLogger } = await import('./Logger');
describe('Logger', () => {
it('should call winston createLogger with format.json when config.json is true', () => {
const config = {
json: true,
logLevel: "info",
};
buildLogger(config);
expect(winston.createLogger).toHaveBeenCalledWith(
expect.objectContaining({
level: "info"
}),
);
});
});
(Logger.ts is the original one.)
Now the module is mocked before importing the logger dependency, which will see the mocked version of winston. Just a few notes here:
__esModule: true is probably not necessary in your case (or maybe I was not able to correctly mock the ES module without dynamic imports), but in case you have to mock an ES module that you will use in the current test file, then you have to use it. (See here)
I had to configure Jest with transform: {}, see here
The upside is that the implementation code stays unchanged, but the test code becomes more complex to handle and maintain. Besides, there could be some situations where this does not work at all.
#3, #4... 🤔
There is at least another solution out there, but, just looking at the method name, I would not use it: I am talking about unstable_mockModule. I have not found official documentation for it, but it is probably not ready for production code.
Manual mocks could be another way to solve this, but I have not tried it.
Honestly, I am not fully satisfied with any of these solutions. In this case, I would probably use the first one, at the expense of the implementation code, but I really hope someone finds something better.
Try mocking:
jest.mock('winston', () => {
return {
createLogger: jest.fn()
}
});
describe('Logger', () => {
...

Setting up jest mocks - one way works the other doesn't

When setting up jest mocks for a class what does not work for me with an error of "_TextObj.TextObj is not a constructor" is
import { TextObj, } from "#entities/TextObj";
jest.mock('#entities/TextObj', () => {
return jest.fn().mockImplementation((config: TextObjConfig) => {
return { ...
}
});
});
According to https://jestjs.io/docs/es6-class-mocks#calling-jestmock-with-the-module-factory-parameter I had expected the first version to work too - or not?
however what works is
import { TextObj, } from "#entities/TextObj";
jest.mock('#entities/TextObj');
...
beforeAll(() => {
TextObj.mockImplementation((config: TextObjConfig) => {
return {
..
}
});
});
TextObj is a named export and you're trying to mock default export which is why it is throwing the error _TextObj.TextObj is not a constructor.
For mocking named export, you need to do following the changes i.e return an object that contains TestObj property:
import { TextObj, } from "#entities/TextObj";
jest.mock('#entities/TextObj', () => {
TestObj: jest.fn().mockImplementation((config: TextObjConfig) => {
return { ...
}
});
});

Jest - Mocking and testing the node.js filesystem

I have created a function which basically loops over an array and create files. I'm starting to get into testing using Jest to have some extra security in place to make sure everything works however I'm experiencing some issues trying to mock the Node.js filesystem.
This is the function I wish to test - function.ts:
export function generateFiles(root: string) {
fs.mkdirSync(path.join(root, '.vscode'));
files.forEach((file) => {
fs.writeFileSync(
path.join(root, file.path, file.name),
fs.readFileSync(path.join(__dirname, 'files', file.path, file.name), 'utf-8')
);
});
}
const files = [
{ name: 'tslint.json', path: '' },
{ name: 'tsconfig.json', path: '' },
{ name: 'extensions.json', path: '.vscode' },
];
I've been reading around but can't really figure out how to test this with jest. No examples to look at. I've tried to install mock-fs which should be a simple way of getting up and running with a mock version of the Node.js FS module but I honestly don't know where to start. This is my first attempt at making a simple test - which causes an error, says 'no such file or directory' - function.test.ts:
import fs from 'fs';
import mockfs from 'mock-fs';
beforeEach(() => {
mockfs({
'test.ts': '',
dir: {
'settings.json': 'yallo',
},
});
});
test('testing mock', () => {
const dir = fs.readdirSync('/dir');
expect(dir).toEqual(['dir']);;
});
afterAll(() => {
mockfs.restore();
});
Anyone who can point me in the right direction?
Since you want to test you implementation you can try this:
import fs from 'fs';
import generateFiles from 'function.ts';
// auto-mock fs
jest.mock('fs');
describe('generateFiles', () => {
beforeAll(() => {
// clear any previous calls
fs.writeFileSync.mockClear();
// since you're using fs.readFileSync
// set some retun data to be used in your implementation
fs.readFileSync.mockReturnValue('X')
// call your function
generateFiles('/root/test/path');
});
it('should match snapshot of calls', () => {
expect(fs.writeFileSync.mock.calls).toMatchSnapshot();
});
it('should have called 3 times', () => {
expect(fs.writeFileSync).toHaveBeenCalledTimes(3);
});
it('should have called with...', () => {
expect(fs.writeFileSync).toHaveBeenCalledWith(
'/root/test/path/tslint.json',
'X' // <- this is the mock return value from above
);
});
});
Here you can read more about the auto-mocking

Testing if external component method is called in jest

I am using jest and enzyme for unit testing. Below is my index.js file. I need to test openNotification and uploadErrorNotification function of the file. However, only uploadErrorNotification function is exported. So, How do I test both the functions.
Also, I don't want to use any other libray except jest and enzyme.
//index.js
import {
notification
} from 'antd';
const openNotification = (message, description, className) => {
notification.open({
key: 'upload-template',
message,
description,
placement: "bottomRight",
duration: null,
});
};
const uploadErrorNotification = (uploadFailedText, errorMsg) => {
openNotification(uploadFailedText, errorMsg, 'error');
};
export {
uploadErrorNotification
}
This is my test file:
//test.js
import { uploadErrorNotification } from '../index.js
jest.mock('notification', () => ({ open: () => jest.fn() })); // was trying this but I couldn't understand how it will work
describe('Notification validation functions testing', () => {
uploadErrorNotification('Upload failed', 'Something went wrong.');
expect("openNotification").toHaveBeenCalledTimes(1); // want to do something like this
});
You have to mock the external depenency:
first mock antd so that notification.open is a spy
jest.mock('antd', () => ({notification: open: {jest.fn()}}))
Then import the module into your test
import { notification } from 'antd';
Know you can use it like this:
expect(notification.open).toHaveBeenCalledTimes(1);
If you want to test notification without overwrite other antd component, you can add jest.requireActual('antd').
jest.mock('antd', () => {
return {
...jest.requireActual('antd'),
notification: {
open: jest.fn(),
},
};
});

Mock.mockImplementation() not working

I have a service class
Service.js
class Service {
}
export default new Service();
And I am trying to provide a mock implementation for this. If I use something like this:
jest.mock('./Service', () => { ... my mock stuff });
It works fine, however I'm not able to access any variables declared outside of the mock, which is a bit limiting as I'd like to reconfigure what the mock returns, etc.
I tried this (inspired by this other StackOverflow article: Service mocked with Jest causes "The module factory of jest.mock() is not allowed to reference any out-of-scope variables" error)
import service from './Service';
jest.mock('./Service', () => jest.fn);
service.mockImplementation(() => {
return { ... mock stuff }
);
Unfortunately when I am trying to run this, I get the below error:
TypeError: _Service2.default.mockImplementation is not a function
I had same problem as #Janos, the other answers didn't help either. You could do two things :
If you need to mock only a function from Service, in your test file:
import service from './Service';
jest.mock('./Service', () => jest.fn());
service.yourFunction = jest.fn(() => { /*your mock*/ })
 
If you need to mock the entire Module:
Say your service.js is in javascript/utils, create a javascript/utils/_mocks_ and inside it create a service.js file, you can then mock the entire class in this file, eg:
const myObj = {foo: "bar"}
const myFunction1 = jest.fn(() => { return Promise.resolve(myObj) })
const myFunction2 = ...
module.exports = {
myFunction1,
myFunction2
}
then in your test file you just add:
jest.mock('./javascript/utils/service')
...functions exported from the mockfile will be then hit through your test file execution.
The mock is equal to jest.fn. You need to call jest.fn to create a mocked function.
So this:
jest.mock('./Service', () => jest.fn);
Should be:
jest.mock('./Service', () => jest.fn());
ran into similar issues and resolved it by using .mockImplementationOnce
jest.mock('./Service', () => jest.fn()
.mockImplementationOnce(() => {
return { ... mock stuff }
})
.mockImplementationOnce(() => {
return { ... mock other stuff }
})
);
now when you run another test it will return the second mock object.
You need to store your mocked component in a variable with a name prefixed by "mock" and make sure you return an object with a default property as you import your Service from the default in your "main.js" file.
// Service.js
class Service {
}
export default new Service();
// main.test.js (main.js contains "import Service from './Service';")
const mockService = () => jest.fn();
jest.mock('./Service', () => {
return {
default: mockService
}
});
I had similar problem, and the cause was that ".spec.js" file had an
import jest from "jest-mock";
After removing this line, it worked.
My mistake was that I was resetting the mock before each test. If you do that, be sure to reconfigure the mock implementation.
For example, change this:
let value;
let onPropertyChange: OnPropertyChangeCallback = jest.fn((changes: any) => {
value = changes["testValue"];
});
const user = userEvent.setup();
beforeEach(() => {
jest.resetAllMocks();
});
to this:
let value;
let onPropertyChange: OnPropertyChangeCallback;
const user = userEvent.setup();
beforeEach(() => {
jest.resetAllMocks();
onPropertyChange = jest.fn((changes: any) => {
value = changes["testValue"];
});
});

Resources