Jest - Unit Tests in Javascript applications

🍱Framework
javascript
test

Interesting examples of tricky things to unit test with the testing framework Jest.

Mocks

Mock API calls with fetch

Use jest-fetch-mock to replace all fetch API calls with mock calls.

Example:

const setupComponent = method =>
    method(
        <HomeWithSearch
            locale={'en_us'}
            allTemplates={TemplateListData}
            onTemplateRowClick={() => {}}
            onTemplateDisabledRowClick={() => {}}
        />
    );


describe('Home Container wrapped in Search HOC', () => {
        beforeEach(() => {
            (fetch as any).resetMocks();
        });

        it('Fetches search tags on mount and renders as expected', () => {
            (fetch as any).mockResponseOnce(
                JSON.stringify([{ id: 1, term: 'UMM' }, { id: 2, term: 'MRI' }])
            );

            setupComponent(shallow);

            // Assert on the times called and arguments given to fetch
            expect((fetch as any).mock.calls.length).toEqual(1);
            expect((fetch as any).mock.calls[0][0]).toEqual(
                'other_company_repo_get_search_term{"locale":"en_us"}'
            );
        });

        it('Matches the snapshot', () => {
            expect(setupComponent(mount).html()).toMatchSnapshot();
        });
    });

Mock an imported module with manual mocks.

Examples:

// Problematic
import { IS_IOS } from 'common/constants/platform';

// Solution
jest.mock('common/constants/platform', () => ({ IS_IOS: false }));
// Problematic
import { ModalVideo } from 'components/Modal';

// Workaround
jest.mock('components/Modal', () => ({
  ModalVideo: () => null,
}));
// Simple workarounds
jest.mock('components/Audio', () => {});
jest.mock('components/CachedImage', () => jest.fn(() => null));
// Another workaround
jest.mock('react-native-sound', () => 'Sound');

Mocking dates

Dates can cause annoying problems.

In case you have such a mock:

{
	valueId: 1,
	reminderId: 1,
	valueType: 'PEF',
	personalBest: 200,
	isCompleted: false,
	timeScheduled: '13:00',
	recordedAt: '2021-08-10T12:00:00.466Z',
},

the line recordedAt: '2021-08-10T12:00:00.466Z', can cause millisecond missmatches in tests on some machines (not all).

To resolve the issue add the following to your test file:

beforeAll(() => {
  // Set date of "today"
  MockDate.set(new Date('2021-08-17T12:00:00.466Z'));
});

afterAll(() => {
  MockDate.reset();
});

Jest config

const enzyme = require('enzyme');
const Adapter = require('enzyme-adapter-react-15');
global.fetch = require('jest-fetch-mock');

enzyme.configure({ adapter: new Adapter() });

jest.setTimeout(30 * 1000);

Enzyme (use it with Jest)

JavaScript Testing utilities for React

shallow, render and mount

Nice, but slightly outdated overview of shallow vs. render vs. mount.

  • shallow vs. mount

    From enzyme version 3 on lifecycle methods in shallow behave like in mount

    Sometimes mount is needed when wrapping the component in e.g. intl. Otherwise only the wrapper would be used.

    	import withIntl from 'utils/testing/intl';
    	import CellRenderer from './activeInactiveUsersCellRenderer';
    	const CellRendererMountable = withIntl('en')(CellRenderer);
    	...
    	it(`should return a status-label of type "${labelTypes.none}" when no cell data is passed`, () => {
    	    expect(
    	      mount(<CellRendererMountable />).find(StatusLabel).props().type,
    	    ).toBe(labelTypes.none);
    
    });

    where withIntl is as the file at the bottom of this file!

create (react-test-renderer) vs. shallow (Enzyme)

  • react-test-renderer can render a React DOM or React Native component without using a browser or the jsdom package.
  • shallow etc. of Enzyme use jsdom internally.

Enzyme Matchers

A (sometimes) useful extension of matchers.

Tipps on useful unit tests and how to test it with Jest

  • Test whether local callback function is passed down as prop

    	const wrapper = shallow(<ExhibitionGrid header={mockHeader} />);
    	expect(wrapper.find('Grid')).toHaveProperty(
    'renderHeader',
    wrapper._renderHeader,
    
    );
  • Refs: refs are not resolved when testing with enzyme, so you have to wrap that in a try/catch to not throw an error.

    	try {
    
    this.\_listRef.scrollToOffset({ offset: scrollPosition, animated: false });
    } catch (e) {
    // eslint-disable-line no-empty
    }
  • Check whether style exists

    const style = { backgroundColor: 'red' };
    
    const wrapper = shallow(<BundleLoader show={true} />);
    
    wrapper.setProps({ show: true, style: style });
    expect(wrapper.first().props().style).toEqual(
    expect.arrayContaining([style]),
    );
  • .findWhere() searches the entire tree of components for something. In React Native you can do .findWhere(n => n.text() === 'some text').

    This example here also shows jest.fn() and simulate('press')

    	it('should execute an onClose callback', () => {
    	    const onClose = jest.fn();
    	    const wrapper = shallow(<BundleLoader show={true} onClose={onClose} />);
    	    wrapper.findWhere(n => n.prop('onPress')).simulate('press');
    	    expect(onClose).toHaveBeenCalled();
    
    });
  • Functions may be found by their name property:

    	const EmptyState = () => null;
    	const renderEmptyState = jest.fn(() => <EmptyState />);
    	...
    	expect(header.find(EmptyState.name).exists()).toEqual(true);
    	```
  • A component MyComponent may be found by the node.type() property

    Here a sub-component is searched for which has a specific prop.

    	const component = (
    <CellRendererMountable inactiveUsers={23} activeUsers={23} />
    
    );
    expect(
    mount(component).findWhere(node => node.type() === MyComponent && node.props().foo === 'myFooProp')
    ).toHaveLength(1);;
  • .dive() while shallow rendering

    	it('should render a progress component and label', () => {
    	    const progress = 0.84115;
    	    const wrapper = shallow(<BundleLoader show={true} progress={progress} />);
    	    expect(wrapper.find('Progress').exists()).toBe(true);
    	    expect(
    	      wrapper
    	        .find('Text')
    	        .dive()
    	        .text(),
    	    ).toEqual(expect.stringContaining('(84%)'));
    
    });
  • spyOn()

    Creates a mock function similar to jest.fn but also tracks calls to object[methodName]. Returns a Jest mock function.

    Here spyOn() is used to check whether the function _updateContainerStyle which is contained inside a wrapper instance is called.

    it('should update the state when new style prop is passed', () => {
        const style = { backgroundColor: 'red' };
        const wrapper = shallow(<BundleLoader show={true} />);
        const inst = wrapper.instance();
        const spy = jest.spyOn(inst, '_updateContainerStyle');
        expect(wrapper.first().props().style).not.toEqual(
          expect.arrayContaining([style]),
        );
    
        wrapper.setProps({ show: true, style: style });
        expect(wrapper.first().props().style).toEqual(
          expect.arrayContaining([style]),
        );
        expect(spy).toHaveBeenCalledTimes(1);
    
        // do not update when style is the same
        wrapper.setProps({ show: true, style: style });
        expect(spy).toHaveBeenCalledTimes(1);

});

## Great helper functions

Use [jest-in-case](https://github.com/atlassian/jest-in-case) to create variations of the same test.

Taken from [Kent C. Dodds blog](https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-tests):

```js
import cases from 'jest-in-case';
import fizzbuzz from '../fizzbuzz';

cases(
  'fizzbuzz',
  ({ input, output }) => expect(fizzbuzz(input)).toBe(output),
  [
    [1, '1'],
    [2, '2'],
    [3, 'Fizz'],
    [5, 'Buzz'],
    [9, 'Fizz'],
    [15, 'FizzBuzz'],
    [16, '16'],
  ].map(([input, output]) => ({
    title: `${input} => ${output}`,
    input,
    output,
  }))
);

Appendix

  • withIntl is an example of a higher order component used for testing. It wraps a component to be tested. It expects a locale, checks whether it’s valid and wraps the provided component with IntlProvider.

    Usage:

    	const MyComponentWithIntl = withIntl('en_US')(MyComponent);
    	```
    
    	**withIntl**:
    
    	```js
    	import React, { createElement } from 'react';
    	import { IntlProvider } from 'react-intl';
    	import first from 'lodash/first';
    
    	import { appLocales, translationMessages } from 'i18n';
    
    	/**
    	 * mocks the intl prop from react-intl to be used in tests
    	 */
    	export const mockIntl = {
    	  formatMessage: ({ defaultMessage }) => defaultMessage,
    	  formatDate: () => '',
    	  formatTime: () => '',
    	  formatRelative: () => '',
    	  formatNumber: () => '',
    	  formatPlural: () => '',
    	  formatHTMLMessage: () => '',
    	  now: () => 1337,
    	};
    
    	export const validateLocale = locale =>
    	  translationMessages[locale] ? locale : first(appLocales);
    
    	export const getMessages = locale => translationMessages[locale];
    
    	export const getSettings = locale => {
    	  const validLocale = validateLocale(locale);
    
    	  return {
    	    locale: validLocale,
    	    messages: getMessages(validLocale),
    	  };
    	};
    
    	export default locale => {
    	  const settings = getSettings(locale);
    
    	  return component => props =>
    	    <IntlProvider {...settings}>
    	      {createElement(component, props)}
    	    </IntlProvider>;
    	};
    	```

Discuss on TwitterImprove this article: Edit on GitHub

Discussion


Explain Programming

André Kovac

André Kovac builds products, creates software, teaches coding, communicates science and speaks at events.

To know which blog posts are the most popular I added Google Analytics to the site. Is that alright with you?