- Common Use Cases
- Advanced Use Cases
- Mocking a module for all tests
- Mocking a module for a single test
- Mocking a majority of a module except for specific exports
- Mocking calls to functions from within an export
- Waiting for internal Promises to complete
- Dealing with Webpack Dynamic Imports
- Mocking a module that doesn't exist
- Unwrap A Connected Component
- Testing selectors that were created with reselect.createSelector
Most of the time you'd just check to see if a function was called with a specific Object.
expect( func ).toHaveBeenCalledWith({
prop: val,
prop2: val2,
});
There are some cases where you may only care that one prop of a default Object has been altered.
expect( func ).toHaveBeenCalledWith(expect.objectContaining({
prop2: customVal,
}));
There will be cases where you need to validate that a function was called with a function, but that function may have been
called with bind
so you'll just have to settle for the fact that it was called with any kind of function.
expect(funcName).toHaveBeenCalledWith(expect.any(Function));
For this to be available to all tests, you'll want to have this in your setup script. So in your Jest configuration you'd
have a property for setupTestFrameworkScriptFile
pointing to something like setup.js
, and inside of that file you'd have:
window.testCtx = {
/**
* Allows for setting `window.location` props within tests
* @param {String} prop - The `location` prop you want to set.
* @param {String} val - The value of the prop.
*/
location: function(prop, val){
Object.defineProperty(window.location, prop, {
writable: true,
value: val
});
},
};
And then inside your test you'd have something like this:
window.testCtx.location('search', '?param=val');
There are cases where the above will have unwanted effects, for example, code that would normally update a location
value,
no longer does so. If you want to maintain the jsDOM functionality, you can use this:
window.history.replaceState('', '', someNewURL);
You may run in to this error when calling pushState
or replaceState
SecurityError
at HistoryImpl._sharedPushAndReplaceState (../../node_modules/jsdom/lib/jsdom/living/window/History-impl.js:97:15)
Sometimes this can be resolved by adding a testURL
in your config that matches the domain that you're using for your
tests. There may be times when you want to use URLs with varying domains. In those cases the SecurityError
will persist.
To get around it, I'll be utilizing the window.testCtx.location
method I mentioned above.
window.testCtx.location('href', currURL);
// mock out replaceState to avoid SecurityError's
jest.spyOn(window.history, 'replaceState');
window.history.replaceState.mockImplementation((state, title, url) => {
window.testCtx.location('href', url);
});
Now any calls to replaceState
will update location.href
to whatever was passed in.
There will be times when you need to fast-forward calls to setTimeout
. You can do so with these methods.
// replaces the timer api's with Jest's
jest.useFakeTimers();
// will fast-forward the time by a specific amount
jest.runTimersToTime(TIME); // older versions of Jest
jest.advanceTimersByTime(TIME); // newer versions of Jest
Imagine you have a block of code like this:
const someFunc = (data) => {
if(!data) throw new Error('ruh-roh');
// the good stuff
};
And in your test you need to validate that an error is thrown
expect( () => someFunc() ).toThrowError('ruh-roh');
// if you don't know the error that will be thrown, just use `toThrow`
expect(Object.keys(VAR).includes('resolve', 'reject')).toBe(true);
it.each([
['anchors', { tag: 'a' }],
['buttons', { tag: 'button' }],
])('should apply a11y attribution for %s', (label, { tag }) => {
wrapper = shallow(<Item {...props} />).dive();
expect(wrapper.find(`${tag}.some-class`).props()).toEqual(expect.objectContaining({
// props you care about
}));
});
The below assumes you have auto-mocking disabled.
Even if you're not using the module that needs mocking in your test, you'll need to import
or require
it so that Jest
can mock it's reference before the file you're trying to test brings it in. Note that it doesn't matter if you
import
/require
at the top of the file, all calls to mock
will be hoisted.
import { namedExport } from 'someModule';
// Auto-mocks the module - all it's functions, props, etc. Downside to this is that it adds to the coverage, and has to
// require the actual module, which could execute auto-initializing code. Starting a Server, creating singletons, etc.
jest.mock('someModule');
// Same as above
jest.mock('someModule', () => jest.genMockFromModule('someModule'));
// Manual mocks don't add to coverage, and don't cause any bleed-over of auto-init code.
// Manual mock an ES module
jest.mock('someModule', () => ({
__esModule: true,
default: {
func: jest.fn(),
},
}));
// Manual mock a CommonJS module
jest.mock('someModule', () => jest.fn());
// locModule
export const v = (process.env.VAR) ? 1 : 2;
export default function func() { console.log(v); }
As of Jest 27.0.0-next.3
, isolateModules
is fixed so this is a little simpler.
import { func } from './locModule';
describe('locModule', () => {
it(('should return the default') => {
expect(func()).toEqual(2);
});
describe('with env var', () => {
let _func;
// for speed, this shouldn't be in a beforeEach, unless your test requires it.
jest.isolateModules(() => {
process.env.VAR = true;
_func = require('./locModule').func;
});
it(('should be gated by env flag') => {
expect(func()).toEqual(1);
});
});
});
Old
In order to mock modules for individual tests, you have to jump through a few more hoops. I've added comments in the below code to describe what's happening.
// the initial load of the module to be used in most tests
import locModule from './locModule';
describe('locModule', () => {
// this function needs a module mocked
describe('funcName', () => {
let exenv;
beforeEach(() => {
// clears the cache for modules that have already been loaded
jest.resetModules();
// mock a module that's loaded by `locModule`
jest.doMock('exenv');
exenv = require('exenv');
// re-load `locModule` - but now it'll use the mocked `exenv`
locModule = require('./locModule');
});
afterEach(() => {
// it's important to `unmock` modules otherwise the mocks can bleed over to other tests
jest.unmock('exenv');
jest.unmock('./locModule');
});
});
});
import locModule from './locModule';
jest.mock('./locModule', () => {
const actualModule = jest.requireActual('./locModule');
const module = jest.genMockFromModule('./locModule');
return {
...module,
funcName: actualModule.funcName,
};
});
This is in relation to the above example. Since you're now using the actual function, it will reference all functions within the actual export, not mocked versions. To alter the export context you have to make a slight alteration to your source to allow for mocking.
// locModule.js
const ctx = {};
const func1 = () => {};
ctx.func1 = func1;
const func2 = () => {};
ctx.func2 = func2;
const func3 = (arg1, arg2) => {
ctx.func1(arg1);
ctx.func2(arg2);
}
ctx.func3 = func3;
export {
ctx,
func1,
func2,
func3
};
// test.js
import * as locModule from './locModule';
describe('func3', () => {
beforeEach(() => {
jest.spyOn(locModule.ctx, 'func1');
locModule.ctx.func1.mockImplementation(jest.fn());
jest.spyOn(locModule.ctx, 'func2');
locModule.ctx.func2.mockImplementation(jest.fn());
});
afterEach(() => {
locModule.ctx.func1.mockRestore();
locModule.ctx.func2.mockRestore();
});
it('should do stuff', () => {
locModule.func3(arg1, arg2);
expect(locModule.ctx.func1).toHaveBeenCalledWith(arg1);
expect(locModule.ctx.func2).toHaveBeenCalledWith(arg2);
});
});
There will be times when async/await
won't work because the method you're testing doesn't return a Promise from a
function that's inside itself, so there'll always be a race condition you can't catch. If you've mocked all the internal
functions and they're returning something like Promise.resolve
or Promise.reject
you can do this to ensure the code
in those callbacks has executed before your assertions.
it('should do something', (done) => {
func();
process.nextTick(() => {
expect(true).toBe(true);
done();
});
});
For a more extreme case where there are multiple promises and I/O operations
process.nextTick(() => {
promiseFunc1();
promiseFunc2();
setImmediate(() => {
expect(func).toHaveBeenCalled();
done();
});
});
Use
setImmediate
if you want to queue the function behind whatever I/O event callbacks that are already in the event queue. Useprocess.nextTick
to effectively queue the function at the head of the event queue so that it executes immediately after the current function completes.
Your source may look something like this
import(/* webpackChunkName: "chunkName" */ 'module-name')
.then(({ default: module }) => {
this.module = module;
});
To test such a scenario, you'll have to wire up Babel to handle dynamic imports first. Install the babel-plugin-dynamic-import-node module.
// in your .babelrc.js file
plugins = [
["dynamic-import-node", { "noInterop": true }],
];
module.exports = { plugins };
Then in your test file you'll need to mock the module you want to import, and wait for the Promise to resolve via
nextTick
.
// at the top of the file...
import module, { namedExport } from 'module-name';
jest.mock('module-name', () => ({
__esModule: true,
default: jest.fn(),
namedExport: jest.fn()
}));
// ... inside a test case
it('should import module', await () => {
await funcThatTriggersImport();
expect(instance.module).toEqual(module);
});
There may be a case where there's a require
for a generated file that won't exist for your tests.
// source file
function fn() {
return {
assets: require('../js/webpack-assets.json');
};
}
import fn from './src';
const MOCK_ASSETS = '{ "fu": "bar" }';
jest.mock('../js/webpack-assets.json', () => MOCK_ASSETS, { virtual: true });
// ... inside a test case
expect(fn()).toEqual(MOCK_ASSETS);
Sometimes a component will be exported within a connect
. When that happens, you can no longer use wrapper.state()
to access a
components internal state because you'd be getting the state
of the connected component, not the internal state of the wrapped
component. In those cases, instead of:
wrapper = mount(<Component {...props} />));
console.log(wrapper.state());
you'd use:
wrapper = mount(shallow(<View {...props} />).get(0));
console.log(wrapper.state());
Reselect caches values returned by a selector. The trick to testing a selector is to simply pass it a unique "state" Object everytime. If you have selectors mocked, and returning custom values without a unique Object passed in, the reselect selector will always return the first mocked out value.
// source
import { createSelector } from 'reselect';
import { dialogOpen, modalOpen } from '../selectors';
export const hasOverlay = createSelector(
dialogOpen,
modalOpen,
(...conditions) => conditions.some(s => s)
);
// test
import { dialogOpen, modalOpen } from '../selectors';
import { hasOverlay } from './selectors';
jest.mock('../selectors');
describe(('selectors') => {
describe(('hasOverlay') => {
it(('should inform us that an overlay is open') => {
dialogOpen.mockReturnValue(false);
modalOpen.mockReturnValue(false);
expect(hasOverlay({})).toBe(false);
dialogOpen.mockReturnValue(true);
modalOpen.mockReturnValue(false);
expect(hasOverlay({})).toBe(true);
dialogOpen.mockReturnValue(false);
modalOpen.mockReturnValue(true);
expect(hasOverlay({})).toBe(true);
});
});
});
The bit above with
was helpful, but doesn't quite work.
worked for me.