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);
Related
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);
I am having trouble mocking a third party dependency. I'm always recieving this error:
Cannot spy the undefined property because it is not a function;
undefined given instead
Here are the details of this issue. First, this is the function I am testing:
File: src/js/mp_wrapper.js
import { Viewer } from 'third-party';
module.exports = {
createViewer: container => {
if (util.isElement(container)) {
return new Viewer(container);
} else {
throw new Error(
'Invalid Element when attempting to create underlying viewer.',
);
}
},
}
Looking at the source code for my third-party, Viewer is very simple and looks like this:
function Viewer(){
// Doing things
}
Viewer.prototype.foo = function(){
}
module.exports = Viewer;
And finally, here is my test.
File: /tests/mp_wrapper.spec.js
import { Viewer } from 'third-party`;
import mp_wrapper from '../src/js/mp_wrapper';
describe('mp_wrapper', () => {
describe('createViewer', () => {
test('returns a new instance of the Viewer class', () => {
const spy = jest.spyOn(Viewer).mockImplementation(() => jest.fn());
// It fails on the line above... -> "Cannot spy the undefined property because it is not a function; undefined given instead"
const testElement = document.createElement(testElement);
let viewer = mp_wrapper.createViewer(testElement);
expect(spy).toHaveBeenCalled();
expect(viewer).toBeInstancecOf(Viewer);
spy.mockRestore();
});
});
});
How can I mock & spy on Viewer itself?
I've done this in the past:
const spy = jest.spyOn(Viewer.prototype, 'foo').mockImplementation(() => jest.fn());
I've also tried default with no luck:
const spy = jest.spyOn(Viewer, 'default').mockImplementation(() => jest.fn());
But now I want to spy on Viewer.
EDIT:
This was my final solution. #brian-lives-outdoors answer is correct, but I didn't describe my problem accurately. The third party library I was trying to mock was slightly more complex because it export a module containing several constructors. It looked like this:
module.exports = {
Viewer: require('./path/Viewer'),
Foo: require('./foo_path/Foo'),
Bar: require('./bar_path/Bar')
}
Then inside ./path/Viewer is was as I previously described above.
Here's what my solution ended up looking like:
import { Viewer } from 'lib';
import mp_wrapper from '../src/js/mp_wrapper';
jest.genMockFromModule('lib');
jest.mock('lib');
describe('mp_wrapper', () => {
describe('createViewer', () => {
test('returns a new instance of the Viewer class and sets the local _viewer property', () => {
const testContainer = document.createElement('div');
const viewer = mp_wrapper.createViewer(testContainer);
expect(Viewer).toHaveBeenCalledWith(testContainer); // Success!
expect(viewer).toBeInstanceOf(Viewer); // Success!
});
});
});
#brian-lives-outdoors What I don't understand is if I comment out the line jest.mock('lib'); above, it doesn't work...Why?
Why isn't genMockFromModule sufficient by itself?
The example code mixes ES6 import/ export syntax with Node module.exports syntax...
...but based on a library that looks like this:
lib.js
function Viewer() { }
Viewer.prototype.foo = function () { }
module.exports = Viewer;
...it would be used like this:
mp_wrapper.js
import Viewer from './lib'; // <= Babel allows Viewer to be used like an ES6 default export
export const createViewer = container => new Viewer(container);
...and to spy on Viewer you would need to mock the entire library in your test:
mp_wrapper.spec.js
import Viewer from './lib';
import { createViewer } from './mp_wrapper';
jest.mock('./lib', () => jest.fn()); // <= mock the library
test('returns a new instance of the Viewer class', () => {
const viewer = createViewer('the container');
expect(Viewer).toHaveBeenCalledWith('the container'); // Success!
expect(viewer).toBeInstanceOf(Viewer); // Success!
});
Note that if the library was an ES6 library then you could spy on the default export directly like this:
import * as lib from './lib';
const spy = jest.spyOn(lib, 'default'); // <= spy on the default export
...but because of the way Babel handles the interop between ES6 and non-ES6 code this approach doesn't work if the library is not ES6.
Edit: response to the follow-up question
jest.genMockFromModule generates a mocked version of the module and returns it.
So for example this:
const mock = jest.genMockFromModule('lib');
...generates a mocked version of lib and assigns it to mock. Note that this does not mean that the mock will be returned when lib is required during a test.
jest.genMockFromModule can be useful when creating a manual mock:
__mocks__/lib.js
const lib = jest.genMockFromModule('lib'); // <= generate a mock of the module
lib.someFunc.mockReturnValue('some value'); // <= modify it
module.exports = lib; // <= export the modified mock
In your final solution you have these two lines:
jest.genMockFromModule('lib');
jest.mock('lib');
This line:
jest.genMockFromModule('lib');
...doesn't actually do anything since it is generating a mock of the module but the returned mock isn't being used for anything.
This line:
jest.mock('lib');
...tells Jest to auto-mock the lib module and is the only line that is needed in this case.
Here is a solution:
util.js
const util = {
isElement() {}
};
module.exports = util;
View.js, the third-party module:
function Viewer() {
// Doing things
console.log('new viewer instance');
}
Viewer.prototype.foo = function() {};
module.exports = { Viewer };
my_wrapper.js:
const { Viewer } = require('./viewer');
const util = require('./util');
module.exports = {
createViewer: container => {
if (util.isElement(container)) {
return new Viewer(container);
} else {
throw new Error('Invalid Element when attempting to create underlying viewer.');
}
}
};
Unit test:
const { Viewer } = require('./viewer');
const my_wrapper = require('./');
const util = require('./util');
jest.mock('./viewer', () => {
return {
Viewer: jest.fn()
};
});
describe('mp_wrapper', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('createViewer', () => {
it('t1', () => {
util.isElement = jest.fn().mockReturnValueOnce(true);
let viewer = my_wrapper.createViewer('el');
expect(util.isElement).toBeCalledWith('el');
expect(viewer).toBeInstanceOf(Viewer);
});
it('t2', () => {
util.isElement = jest.fn().mockReturnValueOnce(false);
expect(() => my_wrapper.createViewer('el')).toThrowError(
new Error('Invalid Element when attempting to create underlying viewer.')
);
expect(Viewer).not.toBeCalled();
});
});
});
Unit test result:
PASS src/stackoverflow/57712713/index.spec.js
mp_wrapper
createViewer
✓ t1 (6ms)
✓ t2 (5ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 50 | 100 | |
index.js | 100 | 100 | 100 | 100 | |
util.js | 100 | 100 | 0 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.134s, estimated 9s
For example, if I have main.js calling a defined in src/lib/a.js, and function a calls node-uuid.v1, how can I stub node-uuid.v1 when testing main.js?
main.js
const a = require("./src/lib/a").a
const main = () => {
return a()
}
module.exports = main
src/lib/a.js
const generateUUID = require("node-uuid").v1
const a = () => {
let temp = generateUUID()
return temp
}
module.exports = {
a
}
tests/main-test.js
const assert = require("assert")
const main = require("../main")
const sinon = require("sinon")
const uuid = require("node-uuid")
describe('main', () => {
it('should return a newly generated uuid', () => {
sinon.stub(uuid, "v1").returns("121321")
assert.equal(main(), "121321")
})
})
The sinon.stub(...) statement doesn't stub uuid.v1 for src/lib/a.js as the above test fails.
Is there a way to globally a library function so that it does the specified behavior whenever it gets called?
You should configure the stub before importing the main module. In this way the module will call the stub instead of the original function.
const assert = require("assert")
const sinon = require("sinon")
const uuid = require("node-uuid")
describe('main', () => {
it('should return a newly generated uuid', () => {
sinon.stub(uuid, "v1").returns("121321")
const main = require("../main")
assert.equal(main(), "121321")
})
})
Bear in mind that node-uuid is deprecated as you can see by this warning
[Deprecation warning: The use of require('uuid') is deprecated and
will not be supported after version 3.x of this module. Instead, use
require('uuid/[v1|v3|v4|v5]') as shown in the examples below.]
About how to stub that for testing would be a bit more harder than before as actually there is no an easy way to mock a standalone function using sinon
Creating a custom module
//custom uuid
module.exports.v1 = require('uuid/v1');
Requiring uuid from the custom module in your project
const uuid = require('<path_to_custom_module>');
Sinon.stub(uuid, 'v1').returns('12345');
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 .
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.