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 builds products, creates software, teaches coding, communicates science and speaks at events.