Skip to content

Instantly share code, notes, and snippets.

@the0neWhoKnocks
Last active October 7, 2024 22:41
Show Gist options
  • Save the0neWhoKnocks/bdac1d09b93b8418d948558f7ab233d7 to your computer and use it in GitHub Desktop.
Save the0neWhoKnocks/bdac1d09b93b8418d948558f7ab233d7 to your computer and use it in GitHub Desktop.
Jest Testing Examples

Jest Testing Examples


Common Use Cases

Testing a function was called with a specific Object property

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,
}));

Testing a function was called with a bound callback

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));

Setting props on window.location

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);

Working with History

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.

Time travel

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

Testing Exceptions

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`

Check if an Object contains multiple keys

expect(Object.keys(VAR).includes('resolve', 'reject')).toBe(true);

Running the same tests with different data

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
  }));
});

Advanced Use Cases

The below assumes you have auto-mocking disabled.

Mocking a module for all tests

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());

Mocking a module for a single test

// 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');
    });
  });
});

Mocking a majority of a module except for specific exports

import locModule from './locModule';

jest.mock('./locModule', () => {
  const actualModule = jest.requireActual('./locModule');
  const module = jest.genMockFromModule('./locModule');
  return {
    ...module,
    funcName: actualModule.funcName,
  };
});

Mocking calls to functions from within an export

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);
  });
});

Waiting for internal Promises to complete

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. Use process.nextTick to effectively queue the function at the head of the event queue so that it executes immediately after the current function completes.

Dealing with Webpack Dynamic Imports

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);
  });

Mocking a module that doesn't exist

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);

Unwrap A Connected Component

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());

Testing selectors that were created with reselect.createSelector

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);
    });
  });
});
@pgriesmer-block
Copy link

The bit above with

jest.spyOn(window.history, 'replaceState');
window.history.replaceState.mockImplementation((state, title, url) => {
  window.testCtx.location('href', url);
});

was helpful, but doesn't quite work.

const replaceStateSpy = jest.spyOn(window.history, 'replaceState');
replaceStateSpy.mockImplementation((state, title, url) => {
  window.testCtx.location('href', url);
});

worked for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment