Test module.hot with Jest - javascript

I'm trying to get coverage to 100% for a module with hot module reloading setup.
In my module I have this:
// app.js
if (module && module.hot) module.hot.accept();
In the test file I am trying to do this
// app.test.js
it('should only call module.hot.accept() if hot is defined', () => {
const accept = jest.fn();
global.module = { hot: { accept } };
jest.resetModules();
require('./app');
expect(accept).toHaveBeenCalled();
}
);
But when I log out module in app.js it shows the require stuff but doesn't contain the hot method set by test.

If you have a variable referencing the module object, then you can inject a mock module object into that variable for the test. For instance, you could do the following:
// app.js
// ...
moduleHotAccept(module);
// ...
export function moduleHotAccept(mod) {
if (mod && mod.hot) {
mod.hot.accept();
}
}
Which can be tested like so:
// app.test.js
import { moduleHotAccept } from './app'
it('should only call hot.accept() if hot is defined', () => {
const accept = jest.fn();
const mockModule = { hot: { accept } };
moduleHotAccept(mockModule);
expect(accept).toHaveBeenCalled();
}
);
it('should not throw if module is undefined', () => {
expect(moduleHotAccept).not.toThrow();
}
);
it('should not throw if module.hot is undefined', () => {
expect(
() => moduleHotAccept({notHot: -273})
).not.toThrow();
}
);

I've needed it as well without the ability to pass it from outside.
My solution was to use a jest "transform" that allows me to modify a bit the code of the file that is using module.hot.
So in order to setup it you need to add:
// package.json
"transform": {
"file-to-transform.js": "<rootDir>/preprocessor.js"
//-------^ can be .* to catch all
//------------------------------------^ this is a path to the transformer
},
Inside preprocessor.js,
// preprocessor.js
module.exports = {
process(src, path) {
if( path.includes(... the path of the file that uses module.hot)) {
return src.replace('module.hot', 'global.module.hot');
}
return src;
},
};
That transformer will replace module.hot to global.module.hot, that means that you can control it value in the tests like so:
// some-test.spec.js
global.module = {
hot: {
accept: jest.fn,
},
};
Hope that it helps.

Related

How to preload a javascript dependency via Jest?

Is it possible to inject the dependency via jest or alternative without having to import 'lib' into the file itself?
Example file.js
// Note: lib is not defined in this file
export const getDep = (str) => {
lib('something');
return str;
}
Example jest.config.js
module.exports = {
globals: {
window: {
lib: () => {} // does not work
},
lib: () => {} // does not work
},
setupFiles: ['<rootDir>/jest/setupFiles.js'], // doesnt work either
// ...other stuff
}
Example jest/setupFiles.js
const lib = () => {};
console.log(lib);
Example file.test.js
import { getDep } from "file.js";
describe('Test injection of global dependency using jest.config', () => {
it("should not break test", () => {
const result = getDep('hello');
expect(result).toEqual('hello')
})
})
Also tried jest - setupFiles
Error ReferenceError: lib is not defined
1 |
2 | export const getDep = (str) => {
> 3 | lib('something');
| ^
4 | return str;
Ok thanks to #jonrsharpe for a partial successful solution.
I had to use jest setupFiles, but also I had to add the dependency to the window object.
window.lib = lib;
const lib = () => {};
window.lib = lib; // Added code here..
console.log(lib);

Can I put my jest mock in a separate file? manual mock

In my get-sites.spec.js I created a mock like this which works perfectly fine:
jest.mock('#aws-sdk/client-secrets-manager', () => {
const SecretsManagerClient = jest.fn().mockImplementation(() => {
const result = {
SecretString: "{\"server\":\"host\",\"database\":\"database\",\"user\":\"userName\",\"password\":\"password\"}"
};
return {
send: jest.fn().mockResolvedValue(result)
};
});
const GetSecretValueCommand = jest.fn().mockImplementation(() => {
return {}
});
return {
SecretsManagerClient,
GetSecretValueCommand
}
});
I've tried creating the following directory structure and moved the mock code to the client-secrets-manager.js file.
__tests__/
|- get-sites.spec.js
__mocks__/
|- #aws-sdk/
|-- client-secrets-manager.js
src/
|- get-sites.js
Then in my get-sites.spec.js file I changed the mock code to jest.mock('#aws-sdk/client-secrets-manager');
When I run the test I get an error: TypeError: Cannot read properties of undefined (reading 'SecretString'). Is there some way to move my mock to a separate file to make it available to all my unit tests that will preserve the functionality?
Turns out I was close. I changed the client-secrets-manager.js file in the mocks folder to look like this:
const SecretsManagerClient = jest.fn().mockImplementation(() => {
return {
send: jest.fn()
};
});
const GetSecretValueCommand = jest.fn().mockImplementation(() => {
return {}
});
module.exports = {
SecretsManagerClient,
GetSecretValueCommand
}
Then in my get-sites.spec.js file I added this line to the test:
const sendResponse = {
SecretString: "{\"server\":\"host\",\"database\":\"database\",\"user\":\"userName\",\"password\":\"password\"}"
};
jest.spyOn(SecretsManagerClient.prototype, 'send').mockResolvedValue(sendResponse);
Then I was able to assert it was called like this:
expect(SecretsManagerClient).toHaveBeenCalledTimes(1);
expect(SecretsManagerClient.prototype.send.mock.calls.length).toEqual(1);

Proxyquire shows error "Cannot find module"

I'm trying to use proxyquire to replace a private function for testing in my Meteor app.
Meteor 1.6.1
meteortesting:mocha#1.1.2
In my parentFunction.js:
import { some function } from 'anotherFile';
function childFunction() {
...
return someValue;
}
export default function parentFunction() {
return childFunction()
}
In my test file:
const proxyquire = require('proxyquire');
if (Meteor.isServer) {
...
describe('parentFunction', () => {
it('uses the mocked child function', () => {
const testThing = proxyquire('./parentFunction', {
'childFunction': () => ({ 'name': 'bob' }),
});
});
}
parentFunction.js is in the same folder as my test file, and just to double check the path, I made sure this works:
import parentFunction from './parentFunction';
But when I run the test, I'm seeing an error:
Error: Cannot find module './parentFunction.js'
What am I doing wrong? I've tried an absolute path, that didn't work. And as far as I can see from the documentation a relative path in the file where proxiquire is required, should be fine.
Thanks for any help!

Invalidate node cache when using Jest

I have a file with object that gets populated with process.env properties:
env.js
console.log('LOADING env.js');
const {
PROXY_PREFIX = '/api/',
USE_PROXY = 'true',
APP_PORT = '8080',
API_URL = 'https://api.address.com/',
NODE_ENV = 'production',
} = process.env;
const ENV = {
PROXY_PREFIX,
USE_PROXY,
APP_PORT,
API_URL,
NODE_ENV,
};
module.exports.ENV = ENV;
Now I try to test this file with different process.env properties:
env.test.js
const envFilePath = '../../config/env';
describe('environmental variables', () => {
const OLD_ENV = process.env;
beforeEach(() => {
process.env = { ...OLD_ENV };
delete process.env.NODE_ENV;
});
afterEach(() => {
process.env = OLD_ENV;
});
test('have default values', () => {
const { ENV } = require(envFilePath);
expect(ENV).toMatchSnapshot();
});
test('are string values (to avoid casting errors)', () => {
const { ENV } = require(envFilePath);
Object.values(ENV).forEach(val => expect(typeof val).toEqual('string'));
});
test('will receive process.env variables', () => {
process.env.NODE_ENV = 'dev';
process.env.PROXY_PREFIX = '/new-prefix/';
process.env.API_URL = 'https://new-api.com/';
process.env.APP_PORT = '7080';
process.env.USE_PROXY = 'false';
const { ENV } = require(envFilePath);
expect(ENV.NODE_ENV).toEqual('dev');
expect(ENV.PROXY_PREFIX).toEqual('/new-prefix/');
expect(ENV.API_URL).toEqual('https://new-api.com/');
expect(ENV.APP_PORT).toEqual('7080');
expect(ENV.USE_PROXY).toEqual('false');
});
});
Unfortunately, even though I try to load the file in every test separately the file gets loaded only once, making the third test fail with:
Expected value to equal:
"dev"
Received:
"production"
P.S. It doesn't fail when I run the test alone.
I also know that env.js loads only once because console.log('LOADING env.js'); gets fired only once.
I tried to invalidate Nodes cache like:
beforeEach(() => {
delete require.cache[require.resolve(envFilePath)];
process.env = { ...OLD_ENV };
delete process.env.NODE_ENV;
});
but require.cache is empty {} before each test so it seems that Jest is somehow responsible for importing the file.
I also tried to run yarn jest --no-cache but didn't help.
So what I want is to load env.js before each test so I can test how it behaves with different node environmental variables.
jest#^22.0.4
You can use jest.resetModules() in beforeEach method to reset the already required modules
beforeEach(() => {
jest.resetModules()
process.env = { ...OLD_ENV };
delete process.env.NODE_ENV;
});
Instead of resetting all modules, you can require modules in isolation by using jest.isolateModules(fn).
For example:
test('are string values (to avoid casting errors)', () => {
jest.isolateModules(() => {
const { ENV } = require(envFilePath);
Object.values(ENV).forEach(val => expect(typeof val).toEqual('string'));
});
});
jest version is: "jest": "^26.6.3". Other answers are correct but didn't explain the reason. The reason why delete require.cache[require.resolve(MODULE_PATH)] does NOT work is because jest doesn't implement the require.cache. See issue#5741.
We should use jest.resetModules() or jest.isolateModules(fn) instead.
E.g.
index.js:
module.exports = { name: 'main' };
index.test.js:
describe('71254501', () => {
let obj1, obj2;
beforeEach(() => {
// This will work
// jest.resetModules();
// This doesn't work
delete require.cache[require.resolve('.')];
});
test('should pass 1', () => {
obj1 = require('.');
obj1.name = 'b';
console.log(obj1);
});
test('should pass 2', () => {
obj2 = require('.');
console.log(obj2);
});
});
Test result:
PASS stackoverflow/71254501/index.test.js
71254501
✓ should pass 1 (13 ms)
✓ should pass 2 (1 ms)
console.log
{ name: 'b' }
at Object.<anonymous> (stackoverflow/71254501/index.test.js:10:13)
console.log
{ name: 'b' }
at Object.<anonymous> (stackoverflow/71254501/index.test.js:14:13)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.606 s, estimated 8 s
As you can see, the require cache of module .is NOT deleted. When test case 1 mutates the obj, test case 2 still requires the same instance of the module which is not expected. We want a fresh module .

Require not behaving as expected

I'm using the proxyquire library, which mocks packages on import.
I'm creating my own proxyquire function, which stubs a variety of packages I use regularly and want to stub regularly (meteor packages, which have a special import syntax):
// myProxyquire.js
import proxyquire from 'proxyquire';
const importsToStub = {
'meteor/meteor': { Meteor: { defer: () => {} } },
};
const myProxyquire = filePath => proxyquire(filePath, importsToStub);
export default myProxyquire;
Now I want to write a test of a file which uses one of these packages:
// src/foo.js
import { Meteor } from 'meteor/meteor'; // This import should be stubbed
export const foo = () => {
Meteor.defer(() => console.log('hi')); // This call should be stubbed
return 'bar';
};
And finally I test it like this:
// src/foo.test.js
import myProxyquire from '../myProxyquire';
// This should be looking in the `src` folder
const { foo } = myProxyquire('./foo'); // error: ENOENT: no such file
describe('foo', () => {
it("should return 'bar'", () => {
expect(foo()).to.equal('bar');
});
});
Note that my last 2 files are nested inside a subfolder src. So when I try to run this test, I get an error saying that the module ./foo couldn't be found, as it is being looked for in the "root" directory, where the myProxyquire.js file is, not the src directory as expected.
You might be able to work around that (expected) behaviour by using a module like caller-path to determine from which file myProxyquire was called, and resolving the passed path relative to that file:
'use strict'; // this line is important and should not be removed
const callerPath = require('caller-path');
const { dirname, resolve } = require('path');
module.exports.default = path => require(resolve(dirname(callerPath()), path));
However, I have no idea of this works with import (and, presumably, transpilers).

Categories

Resources