How to test React components with form elements

I’ve been teaching myself how to write tests for React app. It seems that Enzyme is the de facto standard for it.

Here is what I’ve learned for testing components with form elements (e.g. <input />).

Setting up

First of all, install the modules.

npm install --save enzyme enzyme-adapter-react-16

NOTE: It depends on the version of React.js what number the second module ends with. (Should you use React v15, then install “enzyme-adapter-react-15”)

Then, create a folder dedicated to testing on the same level as the component(s) you’ll write tests for.

For example, if you have a component “App.js” just under “/src/components”, you’ll have “/src/components/__tests__”. It seems that Jest, which works in conjunction with Enzyme and runs test files, detects test files which are named “*.test.js” or “*.spec.js” or put in “__tests__” folder.

How to test if the element has updated its value and state

Typically, the test flow looks like the following.

1, decide whether you create a shallow object (shallow()) or fully render DOM of the target component (mount()) in “beforeEach”.

What are the differences between shallow and full rendering? According to the official doc,

Shallow rendering is useful to constrain yourself to testing a component as a unit, and to ensure that your tests aren’t indirectly asserting on behavior of child components.

Shallow rendering doesn’t fully render children, but it can tell whether the component has specified children.

e.g.

it('renders children when passed in', () => {
    const wrapper = shallow((
      <MyComponent>
        <div className="unique" />
      </MyComponent>
    ));
    expect(wrapper.contains(<div className="unique" />)).to.equal(true);
 });

Whereas…

Full DOM rendering is ideal for use cases where you have components that may interact with DOM APIs or need to test components that are wrapped in higher order components.

One thing to note is that fully rendered DOM should be cleaned up by unmount() after the test is done. If the target component is shallowly rendered, the component’s data is simply stored in the computer’s memory. If the component is fully rendered, the component is running in an environment which mimics an actual browser, and the component will be in the DOM. It seems that the DOM will persist unless you unmount the component manually.

Here is another quote from the doc.

unlike shallow or static rendering, full rendering actually mounts the component in the DOM, which means that tests can affect each other if they are all using the same DOM.

Thus, in order to avoid target components affecting each other, it’s necessary to clean up the virtual DOM by unmounting.

Here is an example of a test with the full render approach.

import React from 'react';
import { mount } from 'enzyme';
import InputBox from '../InputBox';

let wrapped;

beforeEach(() => {
    wrapped = mount(<InputBox />);
});

afterEach(() => { 
    wrapped.unmount(); 
});

If you take the shallow approach, the code above will be like…

import React from 'react'; 
import { shallow } from 'enzyme'; 
import InputBox from '../InputBox'; 

let wrapped; 

beforeEach(() => { 
    wrapped = shallow(<InputBox />); 
});

// "wrapped" doesn't have to be unmounted.

2, find the input element from the component.

it('has an input field for entering a text', () => {
    wrapped.find('input');
});

3, simulate the change event with a mock event object. In the following example, you are going to simulate the “change” event.

it('has an input field for entering a text', () => { 
    wrapped.find('input').simulate('change', {
        target: {value: 'blah blah' }
    }); 
});

4, have the component to update its state.

it('has an input field for entering a text', () => { 
    wrapped.find('input').simulate('change', { 
        target: {value: 'blah blah' } 
    });

    wrapped.update();
});

(Why do we have to do that? It’s because of the asynchronous nature of “setState”. The component doesn’t be re-rendered immediately after “setState” was invoked. So, you need to force the “wrapped” component to be updated.)

5, assert that the value and its corresponding state have been updated.

it('has an input field for entering a text', () => { 
    wrapped.find('input').simulate('change', { 
        target: {value: 'blah blah' } 
    });

    wrapped.update();

    expect(wrapped.find('textarea').prop('value')).toEqual('blah blah');
});

How to test if the element has submitted its value

Whether the form’s submit event was handled can be tested in a similar manner.

In the case of the submit event, you’ll simulate replace “change” with “simulate”.

Then, you’ll asset what the input value looks like after submission.

i.e.

it('empties the input box when the form is submitted', () => { 
    wrapped.find('input').simulate('change', { 
        target: {value: 'blah blah' } 
    });
    wrapped.update(); 
    expect(wrapped.find('input').prop('value')).toEqual('blah blah');
    wrapped.find('form').simulate('submit');
    wrapped.update();
    expect(wrapped.find('input').prop('value')).toEqual('');
    //It is expected that submitting the input box will empty it.
});

What if the target component is wrapped with connect() of Redux?

If the component is connected to Redux, the approaches above will result in the following error.

Error: Uncaught [Invariant Violation: Could not find "store" in either the context or props of "Connect(InputBox)". 
Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(InputBox)".]

This indicates that the target component isn’t connected to Redux in the testing environment, even though it is in the actual environment (i.e. the browser).

When a component is connected to Redux, the component searches for the store property of <Provider store={}> tag in the hierarchy. (Typically, the <Provider> tag is located at the root of the React app) However, in the testing environment, the target component can’t find the store property.

Thus, you need to make sure that the target component is always wrapped with the provider tag.

e.g.

//You can create a component like this.

import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import reducers from './reducers';

export default (props) => {
    return (
        <Provider store={createStore(reducers,{})}>		
            {props.children}	
        </Provider>
    );
}

//Wrap the target componet, as well as the actual root, which is usually named as App.js
beforeEach(() => {
    wrapped = mount(
        <ReduxProvider>
            <CommentBox />
        </ReduxProvider>        
    );
});

Leave a Reply

Your email address will not be published. Required fields are marked *