1. 程式人生 > >Components testing in React: what and how to test with Jest and Enzyme.

Components testing in React: what and how to test with Jest and Enzyme.

Testing React components may be challenging for beginners as well as experienced developers who have already worked with tests. It may be interesting to compare your own approaches with the ones we use in our project. In order to cover the codebase, you have to know which components must be tested and which code exactly in the component should be covered.

During this article, I’ll cover the following topics:

  • Define the correct order of components’ testing based on project structure
  • Find what to omit in test coverage (what not to test)
  • Identify the necessity of Snapshot Testing
  • Define what to test in the component and in which order
  • Provide detailed custom code examples

The article requires that you have knowledge about Jest and Enzyme setup. Information about installation and configuration can be easily found on their official websites.

Assume the following case: You need to cover the project codebase with tests. What should you start with, and what should you get at the end of testing? 100% test coverage? It is the benchmark to which you should aspire, but in most situations you won’t get it.

Why? Because you shouldn’t test all code. We will find out why and what should be left out of tests. Even more, 100% test coverage does not always ensure that the component is fully tested. There is also no guarantee that it will notify you if something has been changed. Don’t strive for the percentages, avoid writing fake tests, and just try not to lose the main component details.

Defining the correct order of the components’ testing based on project structure

Let’s discuss this question on the next part of the project structure:

I took shared directory because it is the most important. It consists of the components that are used in several different pages of the project. They are reusable, and normally they are small and not complex. If one or another component fails, it will cause failing in other places. That’s why we should be confident whether they have been written correctly. The structure of this directory is divided into several folders, each containing components.

How to define the correct order of component testing in the shared directory:

  • Always follow the rule of going from simple to complex. Analyze each directory and define which components are independent - namely, their rendering doesn’t depend on the other components. They are self-completed and can be used separately as a single unit. From the structure above, it is the inputs directory in the forms folder. It contains input components to redux-forms, such as TextInput, SelectInput, CheckboxInput, DateInput, etc.
  • Next, we need to define the auxiliary components that are often used in the inputs components, but should be tested apart from them. This isthe utils directory. Components in this folder are not complicated, but very important. They are frequently reusable and help with repeated actions.
  • The next step is to define which components can be used independently too. If any, take them for testing. From our structure, it is the widgets, the little components with simple functionality. They will be the third item in the queue for test coverage.
  • Further, analyze the rest of the directories and define more complex components, which can be used independently or in conjunction with other components. It is the modals directory in our case. These components will be explained in detail below.
  • The most complex components are left to the end. They are the hoc directory and fields from the forms folder. How do you define which one should be tested first? I take the directory from which components have already been used in tested components. Thus, the component from the hoc directory was present in the widgets component. That’s why I already know where and for which purpose this directory and its component are used.
  • The last one is the fields folder. It contains components connected with redux-forms.

The final components order (based on our example) will look like this:

Following this order, you increase complexity of the tested components step by step. Thus, when it comes to operating with the more complex components, you already know how the smallest ones behave.

Don’t take for testing, for example, the ‘array’ field, if you are not sure how to test the ‘text’ field. Don’t take components decorated with the redux-form if you haven’t tested the ‘form’ field itself.

Be consistent in your choices, don’t take the first component that comes into your mind, and switch the logic. Of course, the structure of your project can differ. It can have other directory names or can have additional components, actions, and reducers, but the logic of defining the order for testing the components is the same.

Let’s define what should be omitted in test coverage:

  • Third-party libraries. Don’t test functionality that is taken from another library. You are not responsible for that code. Skip it or imitate implementation if you need it to test your code.
  • Constants. The name speaks for itself. They are not changeable. They are sets of static code that are not intended to vary.
  • Inline styles (if you use them in your component). In order to test inline styles, you need to duplicate object with styles in your test. If object styles change, you must change them in the test too. Don’t duplicate a component’s code in tests. You will never keep in mind to change it in tests. Moreover, your colleague will never realize that there’s been duplication. In most cases, inline styles don’t change the component’s behaviour, so they shouldn’t be tested. There can be an exception if your styles change dynamically.
  • Things not related to the tested component. Skip covering with tests components that were imported in the tested component. Be careful if it is wrapped in another one. Don’t test the wrapper, just analyze and test them separately.

So how do you actually write tests? I combine two testing approaches:

  • Snapshot Testing
  • Component logic testing

I’ll discuss them both now.

How to test with snapshots

Snapshot Testing is a useful testing tool in case you want to be sure that the user interface hasn’t changed. When facing this testing tool for the first time, you might have questions concerning organization and managing snapshots. The principle is very simple, but unfortunately it has not been fully described anywhere.

Step 1. Write the test for the component, and in the expect block use the .toMatchSnapshot() method that creates the Snapshot itself:

it('render correctly text component', () => {      const TextInputComponent = renderer.create(<TextInput />).toJSON();    expect(TextInputComponent).toMatchSnapshot();});

Step 2. When you run the test for the first time on the one level, along with the test, a directory will be created named __snapshots__ with the autogenerated file inside with the extension.snap.

Snapshot looks like this:

exports[`Render TextInput correctly component 1`] = `  <input    className="input-custom"  disabled={undefined}  id={undefined}  name={undefined}  onBlur={undefined}  onChange={[Function]}  pattern={undefined}  placeholder={undefined}  readOnly={false}  required={undefined}  type="text"  value={undefined}/>`;

Step 3. Push the snapshot into the repository and store it along with the test.

If the component has been changed, you just need to update the snapshot with —updateSnapshot flag or use the short form u flag.

So the Snapshot is created — how does it work?

Let us consider two cases:

1. The component has changed

  • Run tests
  • New snapshot is created, it compares with the auto generated snapshot stored in the directory __snapshots__
  • Tests failed because snapshot is different

2. The component has not changed

  • Run tests
  • New snapshot is created, it compares with the auto generated snapshot stored in the directory __snapshots__
  • Tests passed because snapshot is identical

Everything is fine when we test a small component without logic (just UI rendering). But as practice shows, there are no such components on real projects. If they exist, they are few.

Are there enough snapshots for full component testing?

Main instructions for component testing

1. One component should have only one snapshot.

If one snapshot fails, most likely the others will fail too. Do not create and store a bunch of unnecessary snapshots clogging the space and confusing developers who will read your tests after you.

Of course, there are exceptions when you need to test the behavior of a component in two states: for example, in the state of the component before opening the pop-up and after opening.

However, even such a variant can always be replaced by this one: the first test stores the default state of the component without the popup in snapshot, and the second test simulates the event and checks the presence of a particular class. In this way, you can easily bypass the creation of several snapshots.

2. Testing props

As a rule, I divide the testing of the props into two tests:

  • Firstly, check the render of default prop values. When the component is rendered, I expect a value to be equal to defaultProps in case this prop has defaultProps.
  • Secondly, check the custom value of the prop. I set my own value and expect it to be received after the rendering of the component.

3. Testing data types

In order to test what type of data comes in the props or what kind of data is obtained after certain actions, we can use the special library jest-extended (Additional Jest matchers), which has an extended set of matches that are absent in the Jest. With this library, testing data types is much easier and more enjoyable.

Testing proptypes, on the other hand, is a contradictory question. Some developers can argue against proptypes testing because it is a third-party package and shouldn’t be tested. Still, I insist on testing components’ proptypes because I don’t test the package functionality itself. Instead, I just ensure that the proptypes are correct. Data type is a very important programming part and shouldn’t be skipped.

4. Event testing

After creating a snapshot and covering props with tests, you can be sure that the component will render correctly. But this is not enough for full coverage in case you have events in the component.

You can check event in several ways. The most widely used are:

  • mock event => simulate it => expect event was called
  • mock event => simulate event with params => expect event was called with passed params
  • pass necessary props => render component => simulate event => expect a certain behavior on called event

5. Testing conditions

Very often you can have conditions for the output of a particular class, rendering a certain section of the code, transferring the required props, and so on. Do not forget about this, because with default values, only one branch will pass the test, while the second one will remain untested.

In complex components with calculations and lots of conditions, you can miss some branches. To make sure all parts of the code are covered by tests, use a test coverage tool and visually check which branches are covered and which are not.

6. Testing state

To check state, in most cases, it is necessary to write two tests:

  • The first one checks the current state.
  • The second one checks the state after calling an event. Render component => call function directly in the test => check how state has changed. To call the function of the component, you need to get an instance of the component and only then call its methods (example is shown in the next test).

After you walk through this list of instructions, your component will be covered from 90 to 100%. I leave 10% for special cases that were not described in the article, but can occur in the code.

Examples of Testing

Let’s move to examples and cover components with tests as we’ve described above step by step.

1. Testing a component from forms/inputs.

Take one component from the forms/inputs directory. Let it be DateInput.js, the component for datepicker field.

Code listing for tested component: DateInput.js Looks like:

The DateInput component uses the library react-datepicker, with two utilities:

  • valueToDate (converts value to date)
  • dateToValue (converts date to value)

The package is for manipulating with date and PropTypes is for checking React props.

According to the component code, we can see the list of default props that help the component get rendered:

const defaultProps = {      inputClassName: 'input-custom',    monthsShown: 1,    dateFormat: 'DD.MM.YYYY',    showMonthYearsDropdowns: false,    minDate: moment()};

All props are appropriate for creating the snapshot, except one: minDate: moment(). moment() will give us the current date each time we run the test and the snapshot will fail because it stores outdated date. The solution is to mock this value:

const defaultProps = {      minDate: moment(0)}

We need minDate prop in each rendered component. To avoid props duplication, I create HOC which receives defaultProps and returns a pretty component:

import TestDateInput from '../DateInput';  const DateInput = (props) =>      <TestDateInput        {...defaultProps}        {...props}    />;

Don’t forget about moment-timezone, especially if your tests will be run by developers from another country in a different time zone. They will receive the mocked value, but with a time zone shift. The solution is to set a default time zone:

const moment = require.requireActual('moment-timezone').tz.setDefault('America/Los_Angeles')

Now the date input component is ready for testing:

1.Create snapshot first:

it('render correctly date component', () => {      const DateInputComponent = renderer.create(<DateInput />).toJSON();    expect(DateInputComponent).toMatchSnapshot();});

2.Testing props:

Look through the props and find the important ones. The first prop to test is showMonthYearsDropdowns. If it set to true, the dropdown for month and years is shown:

it('check month and years dropdowns displayed', () => {      const props = {            showMonthYearsDropdowns: true        },        DateInputComponent = mount(<DateInput {...props} />).find('.datepicker');    expect(DateInputComponent.hasClass('react-datepicker-hide-month')).toEqual(true);});

Test the null prop value. This check is required to ensure that the component is rendered without a defined value:

it('render date input correctly with null value', () => {      const props = {            value: null        },        DateInputComponent = mount(<DateInput {...props} />);    expect((DateInputComponent).prop('value')).toEqual(null);});

3.Test proptypes for value, date expected to be string:

it('check the type of value', () => {      const props = {            value: '10.03.2018'        },        DateInputComponent = mount(<DateInput {...props} />);    expect(DateInputComponent.prop('value')).toBeString();});

4.Test events:

First, check the onChange event.

  • mock onChange callback
  • render date input component
  • simulate change event with new target value
  • and finally check that onChange event has been called with new value.
it('check the onChange callback', () => {      const onChange = jest.fn(),        props = {            value: '20.01.2018',            onChange        },        DateInputComponent = mount(<DateInput {...props} />).find('input');    DateInputComponent.simulate('change', { target: {value: moment('2018-01-22')} });    expect(onChange).toHaveBeenCalledWith('22.01.2018');});

Next, ensure that the datepicker popup opens after a click on the date input. For that, find date input => simulate click event => and expect popup when class .react-datepicker is present.

it('check DatePicker popup open', () => {      const DateComponent = mount(<DateInput />),        dateInput = DateComponent.find("input[type='text']");    dateInput.simulate('click');    expect(DateComponent.find('.react-datepicker')).toHaveLength(1);});

2. Utility testing:

Code listing for tested utility: valueToDate.js

The purpose of this utility is transforming a value to a date with a custom format.

First of all, let’s analyze the given utility and define the main cases for testing:

  1. According to the purpose of this utility, it transforms value, so we need to check this value:
  • In case value is not defined: we need to be sure that the utility will not return exception (error).
  • In case value is defined: we need to check that the utility returns the moment date.

2. The returned value should belong to the moment class. That’s why it should be an instance of moment.

3. The second argument is dateFormat. Set it as constant before tests. That’s why it will be passed in each test and return value according to the date format. Should we test dateFormat separately? I suppose no. This argument is optional — if we don’t set dateFormat, the utility won’t break, and it’ll just return date in default format. It is a moment job, we shouldn’t test third-party libraries. As I mentioned before, we shouldn’t forget about moment-timezone; it is a very important point, especially for developers from different time zones.

Let’s code:

  1. Write the test for the first case. When we don’t have a value, it is empty.
const format = 'DD.MM.YYYY';
it('render valueToDate utility with empty value', () => {      const value = valueToDate('', format);    expect(value).toEqual(null);});

2. Check if value is defined.

const date = '21.11.2015',        format = ‘DD.MM.YYYY’;
it('render valueToDate utility with defined value', () => {      const value = valueToDate(date, format);    expect(value).toEqual(moment(date, format));});

3. Check if the value belongs to the moment class.

const date = '21.11.2015',      format = 'DD.MM.YYYY';
it('check value is instanceof moment', () => {      const value = valueToDate(date, format);    expect(value instanceof moment).toBeTruthy();});

3. Widgets testing

For widgets testing, I took a spinner component.

Code listing for tested widget: Spinner.js

Looks like this:

The spinner is not required in the explanation, as almost all web resources have this component.

So if we go to write tests:

  1. First step — create snapshot:
it('render correctly Spinner component', () => {     const SpinnerComponent = mount(<Spinner />);   expect(SpinnerComponent).toMatchSnapshot();});

2. Testing props:

First, we look at default prop title, and check if it renders correctly.

it('check prop title by default', () => {   const SpinnerComponent = mount(<Spinner />);    expect(SpinnerComponent.find('p').text()).toEqual('Please wait');});

Then we check the custom prop title. We need to check that it returns the correctly defined prop. Take a look at the code, the title is wrapped in rawMarkup util, and outputs with the help of dangerouslySetInnerHTML property.

Code listing for rawMarkup util:

export default function rawMarkup(template) {      return {__html: template};}

Do we need to include tests for rawMarkup in the spinner component? No, it is a separate utility and it should be tested apart from the spinner. We don’t care how it works — we just need to know that title prop returns the correct result.

Clarification: The reason for using the dangerouslySetInnerHTML property is the following. Our site is multilingual, for which the translations marketing team is responsible. They can translate it simply with a combination of words or even decorate with the HTML tags, like <strong>, <i>, <s> or even slice text with the lists <ol>, <ul>. We don’t know for sure how they translate and decorate the text. We just need to correctly render all this stuff.

I combined two main test cases in one test:

  • return correct custom prop title
  • render prop title correctly with HTML tags
it('check prop title with html tags', () => {      const props = {            title: '<b>Please wait</b>'        },        SpinnerComponent = mount(<Spinner {...props} />);    expect(SpinnerComponent.find('p').text()).toEqual('Please wait');});

Take the next prop subTitle. It is optional and that’s why it doesn’t have a default prop, so skip the step with default props and test custom props:

  • Check that text in subTitle prop renders correctly:
const props = {          subTitle: 'left 1 minute'    },    SpinnerComponent = mount(<Spinner {...props} />);
it('render correct text', () => {      expect(SpinnerComponent.find('p').at(1).text()).toEqual(props.subTitle);});

We know that subTitle is optional. That’s why we need to check whether it is not rendered with default props, according to the slicing markup. Just check the number of tags <p>:

it('check subTitle is not rendered', () => {    const SpinnerComponent = mount(<Spinner />);    expect(SpinnerComponent.find('p').length).toEqual(1);});

3.Testing prop types:

  • For title prop expected to be string:
it('check prop type for title is string', () => {      const props = {            title: 'Wait'        },        SpinnerComponent = mount(<Spinner {...props} />);    expect(SpinnerComponent.find('p').text()).toBeString();});
  • For subTitle prop also expected to be string:
const props = {          subTitle: 'left 1 minute'    },    SpinnerComponent = mount(<Spinner {...props} />);
it('type for subTitle is string', () => {      expect(SpinnerComponent.find('p').at(1).text()).toBeString();});

4. Modals testing (ModalWrapper.js and ModalTrigger.js)

Looks like:

How to test modals

First of all, I want to explain how modals are organized on our project. We have two components: ModalWrapper.js and ModalTrigger.js.

ModalWrapper is responsible for popup layout. It contains the modal container, the button ‘close’, the modal title and body.

ModalTrigger is responsible for modal handling. It includes ModalWrapper layout and contains events for modal’s layout control (open and close actions).

I’ll go over each component separately:

1.Code listing for tested component: ModalWrapper.js

Let’s code:

First, the ModalWrapper receives the component and renders it inside. First of all, check that ModalWrapper won’t fail without the component. Create a snapshot with default props:

it('without component', () => {      const ModalWrapperComponent = shallow(<ModalWrapper />);    expect(ModalWrapperComponent).toMatchSnapshot();});

The next step is to simulate its actual condition with component rendering passed through props:

it('with component', () => {     const props = {           component: () => {}        },        ModalWrapperComponent = shallow(<ModalWrapper {...props} />);    expect(ModalWrapperComponent).toMatchSnapshot();});

Testing props

Receiving custom class name prop:

it('render correct class name', () => {      const props = {            modalClassName: 'custom-class-name'        },        ModalWrapperComponent = shallow(<ModalWrapper {...props} />).find('Modal');        expect(ModalWrapperComponent.hasClass('custom-class-name')).toEqual(true);});

Receiving custom title prop:

it('render correct title', () => {      const props = {           title: 'Modal Title'       },       ModalWrapperComponent = shallow(<ModalWrapper {...props} />).find('ModalTitle');    expect(ModalWrapperComponent.props().children).toEqual('Modal Title');});

Receiving correct show prop:

it('check prop value', () => {        const props = {               show: true           },           ModalWrapperComponent = shallow(<ModalWrapper {...props} />).find('Modal');        expect(ModalWrapperComponent.props().show).toEqual(true);    });

Testing proptypes

  • For show prop
it('check prop type', () => {      const props = {           show: true        },        ModalWrapperComponent = shallow(<ModalWrapper {...props} />).find('Modal');    expect(ModalWrapperComponent.props().show).toBeBoolean();});
  • For onHide prop
it('render correct onHide prop type', () => {      const props = {            onHide: () => {}        },        ModalWrapperComponent = shallow(<ModalWrapper {...props} />).find('Modal');    expect(ModalWrapperComponent.props().onHide).toBeFunction();});
  • For component prop
it(‘render correct component prop type’, () => {     const props = {           component: () => {}       },       ModalWrapperComponent = mount(<ModalWrapper {...props} />);   expect(ModalWrapperComponent.props().component).toBeFunction();});

2.Code listing for tested component: ModalTrigger.js

The modal wrapper has been covered with a test. The second part is to cover the modal trigger component.

Component overview: it is based on the state toggled that indicates visibility of ModalWrapper. If toggled: false, the popup is hidden, else it is visible. The function open() opens the popup on the child element. The click event and function close() hides the popup on the button rendered in the ModalWrapper.

Snapshot creation:

it('render ModalTrigger component correctly', () => {      const ModalTriggerComponent = shallow(<ModalTrigger><div /></ModalTrigger>);    expect(ModalTriggerComponent).toMatchSnapshot();});

Should we test ModalTrigger with component prop rendering? No — because component will be rendered inside the ModalWrapper component. It does not depend on the tested component. It was already covered with tests in the ModalWrapper tests.

Testing props:

We have one prop children and we want to be sure that we have only one child.

it('ensure to have only one child (control element)', () => {      expect(ModalTriggerComponent.findWhere(node => node.key() === 'modal-control').length).toEqual(1);});

Testing proptypes:

The child prop should be an object, so check this in the next test:

const ModalTriggerComponent = mount(<ModalTrigger><div /></ModalTrigger>);
it('check children prop type', () => {        expect(ModalTriggerComponent.props().children).toBeObject();});

An important part of the ModalTrigger component is to check states.

We have two states:

  • Popup is opened. To know that the modal is opened, we need to check its state. For this, call the open function from the instance of the component and expect that toggled in state should be true.
it('check the modal is opened', () => {      const event = {        preventDefault: () => {},        stopPropagation: () => {}    };    ModalTriggerComponent.instance().open(event);    expect(ModalTriggerComponent.state().toggled).toBeTruthy();});
  • Popup is closed. It is tested vice versa, toggled in state should be false.
it('check the modal is closed', () => {     ModalTriggerComponent.instance().close();   expect(ModalTriggerComponent.state().toggled).toBeFalsy();});

Now modals are fully tested. One piece of advice for testing the components that are dependent on each other: look through the components first and write the test plan, define what you need to test in each component, check test cases for each component, and be sure you don’t repeat the same test case in both components. Carefully analyze possible and optimal variants for test coverage.

5. HOC testing (Higher-Order Component)

The last two parts (HOCs and form fields testing) are interconnected. I would like to share with you how to test field layout with its HOC.

Here’s an explanation of what the BaseFieldLayout is, why we need this component, and where we use it:

  • BaseFieldLayout.js is the wrapper for the form input components like TextInput, CheckboxInput, DateInput, SelectInput, etc. Their names end with the -Input because we use redux-form package and these components are the input components to redux-form logic.
  • We need BaseFieldLayout for creating the layout for form fields components, that is rendering label, tooltips, prefixes (currency, square meter abbreviations, etc.), icons, errors and so on.
  • We use it in BaseFieldHOC.js for wrapping the inputComponent in field layout and connecting it with the redux-form with the help of the <Field/> component.

Code listing for tested component: BaseFieldHOC.js

It is a HOC which receives the form input component and returns the component, connected with redux-form.

Analyzing the HOC:

  • This component receives only one prop, component. First of all, I need to create this component and wrap it in the BaseFieldHOC.
  • Next, I need to decorate the wrapped HOC with redux-form in order to get the field connected with redux-form.
  • Render this field inside React Redux <Provider> component to make the store available to the tested component. To mock the store, just do:
const store = createStore(() => ({}));

Now, before each test, I need to do the following:

let BaseFieldHOCComponent;
beforeEach(() => {      const TextInput = () => { return 'text input'; },        BaseFieldHOCWrapper = BaseFieldHOC(TextInput),        TextField = reduxForm({ form: 'testForm' })(BaseFieldHOCWrapper);    BaseFieldHOCComponent = renderer.create(        <Provider store={store}>            <TextField name="text-input" />        </Provider>    ).toJSON();});

After that, the component is ready for testing:

  1. Create snapshot:
it('render correctly component', () => {      expect(BaseFieldHOCComponent).toMatchSnapshot();});

2. Ensure that the input component is wrapped in BaseFieldLayout after rendering:

it('check input component is wrapped in BaseFieldLayout', () => {      expect(BaseFieldHOCComponent.props.className).toEqual('form-group');});

That’s all, the HOC is covered. The most complicated part in testing components connected with redux-form is to prepare the field (decorate with redux form and setup store). The rest is easy, just follow the instructions and nothing else.

6. Forms/fields testing

The field HOC is covered with tests so we can move to BaseFieldLayout component.

Code listing for tested component: BaseFieldLayout.js

Let’s code BaseFieldLayout.js and write the tests according to the instructions above:

  1. First of all, create snapshot.

This component will not be rendered without defaultProps:

  • inputComponent
  • The props provided by redux-form: input and meta objects. Input with property name and meta with properties error and touched:
const defaultProps = {     meta: {        touched: null,        error: null    },    input: {        name: 'field-name'    },    inputComponent: () => { return 'test case'; }}

To use defaultProps in each tested wrapper, do the following:

import TestBaseFieldLayout from '../BaseFieldLayout';
const BaseFieldLayout = (props) => <TestBaseFieldLayout {...defaultProps} {...props} />;

Now we are ready to create snapshot:

it('render correctly BaseFieldLayout component', () => {      const BaseFieldLayoutComponent = renderer.create(<BaseFieldLayout />).toJSON();    expect(BaseFieldLayoutComponent).toMatchSnapshot();});

2. Testing props:

This component has many props. I will show examples of several, and the rest will be tested by analogy.

  • Ensure that the icon prop is rendered correctly