DEV Community

Cover image for Complete Guide on Unit and Integration Testing of React/Redux Connected Forms
Django Stars for Django Stars

Posted on • Edited on • Originally published at djangostars.com

Complete Guide on Unit and Integration Testing of React/Redux Connected Forms

After seeing the amazing feedback and requests that followed my latest article on What and How to Test with Jest and Enzyme, I’d love to share a couple of other test cases. Today, we’ll talk about testing React forms that are connected with Redux, considering both unit and integration testing. Hopefully, you’ll find something useful below.

Unit vs. Integration Testing

Before we dive deep into the subject, let’s make sure we all understand the basics. There are many different types of app testing, but a 2018 survey shows that automated unit and integration tests are at the top of the list.

types-of-testing
For a better comparison, I’m only picking the two main automated testing methods. Let’s look at the definitions and characteristics of unit and integration testing:

automated-testing-methods

Test Preparations: Form Review

Before you start any job, you want to know all about it. You don’t want any surprises, and you do want the best results. This is also true for testing. Which is why it’s better to get all the available information on the form that should be tested, and its related conditions beforehand. And, of course, to make sure you know what exactly should be tested.

To show you how it goes, I chose a form that contains information on Property Evaluation. It’s a form that customers fill in to describe the property they would like to purchase. It’s quite simple – it doesn’t have any complex logic or mandatory fields, and a few fields to fill in.

Check out the image below:

moneypark-form

The only logic you can’t see in the image are different fields that are set depending on the choice in the Property type field. For instance:

  • If a customer chooses ‘Apartment’, they get options like ‘floor’, ‘parking conditions’, etc.
  • If a customer chooses ‘House’, they get options like ‘floor area’, ‘building standard’, etc.

Next, let’s dive into the form’s code. The implementation of the form is divided into two parts:

Testing Forms Connected with Redux

testing-forms-connected-with-redux

Depending on the type of testing, I use different processes to test forms connected with Redux.

For unit tests, I use shallow rendering (as opposed to deep tree rendering) and the Redux-mock-store library. For integration tests, I use mount rendering (deep tree rendering) and an actual Redux store.

Unit Testing of Forms Connected with Redux

As I said above, for unit testing I use shallow rendering. It’s a one-level rendering that doesn’t take into account child components inside the component in question. On top of this, the tested component doesn’t indirectly affect its child components behavior.

Redux-mock-store is a library designed to test action logic, and provides a mocked Redux store. It’s easy to launch and to use, and doesn’t affect the Redux store itself.

Before you start testing, be sure to configure the form.

These are my imports:

  • Rendering method: Enzyme's shallow renderer
  • Include mocked data required for form rendering. In the example below it is json file djangoParamsChoices, containing mocked data for select options. This data is being passed to context on the backend side and fetched on the frontend side through the custom function getDjangoParam.
  • Include form view itself
  • Import additional tools for store mock
  • Import additional libraries for test needs (mainly required when writing the special test case)
import { shallow } from 'enzyme';
import djangoParamsChoices from 'tests/__mocks__/djangoParamsChoices';
import PropertySelfOwnedForm from '../PropertySelfOwnedForm';
import configureStore from 'redux-mock-store';
const snapshotDiff = require('snapshot-diff');
  • Initialize mockstore with empty state:
const initialState = {};
  • Set default props (they vary from the tested form requirements):

The form view depends on the property type; that’s why I put in default props.

const defaultProps = {
    propertyType: 1
};
  • Mock store and render form before each test:

First, configure the mock store with the help of the redux-mock-store library.

const mockStore = configureStore();
  • Configure function for execution before each test run using the ‘beforeEach’ method.
let store, PropertySelfOwnedFormComponentWrapper, PropertySelfOwnedFormComponent, receivedNamesList;

beforeEach(() => {
    store = mockStore(initialState);
    PropertySelfOwnedFormComponentWrapper = (props) => (
        <PropertySelfOwnedForm {...defaultProps} {...props} store={store} />
    );
    PropertySelfOwnedFormComponent = shallow(<PropertySelfOwnedFormComponentWrapper />).dive();
});

Inside the function, don’t forget to:

  • Reset the store after every test: store = mockStore(initialState) returns an instance of the configured mock store.
  • Make Wrapper HOC to pass store, defaultProps and custom props for the special test case

  • Do the form rendering using the .dive() method to receive the rendered form structure one level deeper.

Without the dive() method, ShallowWrapper looks like this:

<PropertySelfOwnedForm
     propertyType={1}
     onSubmit={[Function: mockConstructor]}
     onSubmitAndNavigate={[Function: mockConstructor]}
     onNavigate={[Function: mockConstructor]}
     store={{...}}
/>

Here’s what it looks like with the dive() method: ShallowWrapperWithDiveMethod.js

Writing Tests for Unit Testing

Now, you’re ready to write the test itself. Follow my process to see how you should proceed.

Check the Form Component that’s being rendered:

it('render connected form component', () => {
    expect(PropertySelfOwnedFormComponent.length).toEqual(1);
});

Check the list of fields rendered correctly for property type ‘House’:

it('PropertySelfOwnedForm renders correctly with PropertyTypeHouse', () => {
    receivedNamesList = PropertySelfOwnedFormComponent.find('form').find('Col').children().map(node => node.props().name);
    const expectedNamesList = [
         'building_volume',
         'site_area',
         'building_volume_standard',
         'number_of_garages_house',
         'number_of_garages_separate_building',
         'number_of_parking_spots_covered',
         'number_of_parking_spots_uncovered'
    ];
    expect(receivedNamesList).toEqual(expect.arrayContaining(expectedNamesList));
});

Create a snapshot to check the user interface for property type ‘House’:

it('create snapshot for PropertySelfOwnedForm with PropertyTypeHouse fields', () => {
    expect(PropertySelfOwnedFormComponent).toMatchSnapshot();
});

At this point, you must be asking yourself, “Why do we need two tests for one property type, both snapshot and field existence?” Here’s why: the two tests help us check logic and UI.

  • According to the logic, we should receive an expected list of fields
  • According to the UI, we should obtain a defined order of fields with its own design.

This is what we get from the two tests:

  • No changes in field list / UI -> Two tests passed
  • No changes in field list / Changes in UI -> Snapshot test failed, i.e., the UI changed.
  • Changes in field list / Changes in UI -> Both tests failed, i.e., the logic failed (or both logic and UI), as the field list differs from what was expected.

Having gone through two tests, I see exactly what the problem was and where I should look for reasons for failure. I repeat the process with another property type – ‘Apartment’ and its expected array of fields. I follow the same steps:
Check the list of fields rendered correctly for property type ‘Apartment’:

it('PropertySelfOwnedForm renders correctly with PropertyTypeApartment', () => {
    const props = {
            propertyType: 10
        },
        PropertySelfOwnedFormComponent = shallow(<PropertySelfOwnedFormComponentWrapper {...props} />).dive();
    const receivedNamesList = PropertySelfOwnedFormComponent.find('form').find('Col').children().map(node => node.props().name);
    const expectedNamesList = [
        'number_of_apartments',
        'floor_number',
        'balcony_terrace_place',
        'apartments_number_of_outdoor_parking_spaces',
        'apartments_number_of_garages',
        'apartments_number_of_garages_individual'
    ];
    expect(receivedNamesList).toEqual(expect.arrayContaining(expectedNamesList));

Create a snapshot to check fields for property type ‘Apartment’:

it('create snapshot for PropertySelfOwnedForm with PropertyTypeApartment fields', () => {
    const props = {
            propertyType: 10
        },
        PropertySelfOwnedFormComponent = shallow(<PropertySelfOwnedFormComponentWrapper {...props} />).dive();
    expect(PropertySelfOwnedFormComponent).toMatchSnapshot();
});

The next test is experimental. I decided to investigate a diffing snapshot utility for Jest that one reader of my recent article suggested.

First of all, let’s see how it works. It receives two rendered components with different states or props, and outputs their difference as a string. In the example below, I created a snapshot showing the difference between the forms with different property types – ‘House and ‘Apartment’.

it('snapshot difference between 2 React forms state', () => {
    const props = {
            propertyType: 10
        },
        PropertySelfOwnedFormComponentApartment = shallow(<PropertySelfOwnedFormComponentWrapper {...props} />).dive();
    expect(
        snapshotDiff(
            PropertySelfOwnedFormComponent,
            PropertySelfOwnedFormComponentApartment
        )
    ).toMatchSnapshot();
});

This kind of test has its advantages. As you can see above, it covers two snapshots and minimizes code base – thus, instead of two snapshots, you can create just one showing the difference, and similarly, only write one test instead of two. It’s quite easy to use, and lets you cover different states with one test. But, looking at my case, I got a snapshot with 2841 rows, as shown on github. With a large amount of code like this, it’s too hard to see why the test had failed and where.

This only proves one thing: whatever you work with, use your tools and libraries wisely, and only in the places that really require it. This utility may be useful for testing differences in stateless components to find UI inconsistencies, and to define differences in simple functionality components that contain minimum logical conditions. But for testing large pieces of UI, it doesn’t seem to be appropriate.

Before we wrap up the part about unit testing of forms connected with Redux, there’s one more thing. There’s a reason why I didn’t include tests for events. Let’s look at the form structure PropertySelfOwnedForm.js that includes the ButtonsToolbar.js component.

import ButtonsToolbar from 'shared/widgets/buttons/ButtonsToolbar';
<ButtonsToolbar {...restProps} />

This component contains three buttons: ‘Save’, ‘Save and Next’ and ‘Next’, and is used in many forms. Shallow rendering doesn’t include child components, and at this point, I don’t care about their functionality. And rendered ButtonsToolbarcomponent looks like this:

<ButtonsToolbar
      onNavigate={[MockFunction]}
      onSubmit={[MockFunction]}
      onSubmitAndNavigate={[MockFunction]}
      store={
        Object {
          "clearActions": [Function],
          "dispatch": [Function],
          "getActions": [Function],
          "getState": [Function],
          "replaceReducer": [Function],
          "subscribe": [Function],
        }
      }
    />

The truth is, I don’t need to test it as a part of unit form tests. I will test the button events separately in ButtonsToolbar.test.js. You can find the full tests listing here: PropertySelfOwnedFormUnit.test.js

Integration Testing of Forms Connected with Redux

For integration testing – testing components in a working environment – I use mount rendering. Mount rendering is a type of deep level rendering that includes all the child components by mounting them all into the DOM.

This kind of rendering is actually quite similar to the real DOM tree, as its components’ behavior is interconnected. And the goal of the integration testing is to check this connection. Thus, an actual Redux store is in this case a great choice.

An actual Redux store is one created with the help of a redux library. In this case, there’s no need to mock anything, as you can use the real store the same way as in the app.

Next, I’m configuring my form for testing.

Here’s the list of imports:

  • Rendering method: Enzyme's mount renderer
  • Methods from Redux for creating a store and combining reducers into a single root reducer
  • Provider from react-redux library to make store available for nested components wrapped in the connect() function
  • Router from react-router-dom to provide React Router navigation
  • Redux-form for better managing the redux state of the form
  • propertyDetailsResource is an object with namespace and endpoint
  • Include json file djangoParamsChoices, containing mocked data passed from the backend
  • Include form view itself
import { mount } from 'enzyme';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { reduxForm, reducer as formReducer } from 'redux-form';

import propertyDetailsResource from 'store/propertyDetailsResource';
import djangoParamsChoices from 'tests/__mocks__/djangoParamsChoices';
import PropertySelfOwnedForm from '../PropertySelfOwnedForm';

Then, I prepare data for testing. To do so, it’s important to keep in mind that:

  • There’s a configuration difference between defaultProps for unit and integration tests:

  • With integration tests, a resource with an actual endpoint is added to defaultProps

  • Mocked function handleSubmit is provided by the 'redux-form', because Redux-Form decorates the component with the handleSubmit prop

  • Three mocked functions for custom buttons submit events.

  • The store is created the same way as in the app

  • The imported form is decorated with reduxForm

  • The decorated form is wrapped by Router and Provider.

If it makes it easier for you, the sequence of data preparation for integration testing is the same as it is for actions during the form integration with Redux.

global.getDjangoParam = () => djangoParamsChoices;

let PropertySelfOwnedFormComponent;
const history = {
        push: jest.fn(),
        location: {
            pathname: '/en/data-collection/property-valuation/'
        },
        listen: () => {}
    },
    defaultProps = {
        propertyType: 1,
        resource: propertyDetailsResource,
        handleSubmit: (fn) => fn,
        onSubmit: jest.fn(),
        onSubmitAndNavigate: jest.fn(),
        onNavigate: jest.fn()
    },
    store = createStore(combineReducers({ form: formReducer })),
    Decorated = reduxForm({
        form: 'property-details-form'
    })(PropertySelfOwnedForm),
    PropertySelfOwnedFormComponentWrapper = (props) => (
        <Provider store={store}>
            <Router history={history}>
                <Decorated {...defaultProps} {...props} />
            </Router>
        </Provider>
    );

Render form before each test:

beforeEach(() => {
    PropertySelfOwnedFormComponent = mount(
        <PropertySelfOwnedFormComponentWrapper />
    );
});

Writing Tests for Integration Testing

Now, let’s do the actual writing. The first step is to create snapshots of both property types. This means that, first, you create a snapshot to check the fields for property type House’:

it('PropertySelfOwnedForm renders correctly with PropertyTypeHouse', () => {
    expect(PropertySelfOwnedFormComponent).toMatchSnapshot();
});

Next, create a snapshot to check fields for property type ‘Apartment’:

it('PropertySelfOwnedForm renders correctly with PropertyTypeApartment', () => {
    const props = {
            propertyType: 10
        },
        PropertyTypeApartmentWrapper = mount(<PropertySelfOwnedFormComponentWrapper {...props} />);
    expect(PropertyTypeApartmentWrapper).toMatchSnapshot();
});

The form buttons are disabled if the form is a pristine or in submitting state. The following test checks if the ‘Save’ button reacts to form changes and becomes active after losing the pristine state:

it('check if `Save` button react to form changes', () => {
    expect(PropertySelfOwnedFormComponent.find('button.button--accent').props().disabled).toEqual(true);
    const streetNumberField = PropertySelfOwnedFormComponent.find('input[name="street_number"]');
    streetNumberField.simulate('change', { target: {value: '10'} });
    expect(PropertySelfOwnedFormComponent.find('button.button--accent').props().disabled).toEqual(false);
});

The last three tests check events that are called by clicking on the onSubmit, onSubmitAndNavigate, or the onNavigate button.

Check if an onSubmit event was called:

it('check event on `Save` button', () => {
    const streetNumberField = PropertySelfOwnedFormComponent.find('input[name="street_number"]');
    streetNumberField.simulate('change', { target: {value: '10'} });

    const propertySelfOwnedFormButton = PropertySelfOwnedFormComponent.find('button.button--accent');
    propertySelfOwnedFormButton.simulate('click');
    expect(defaultProps.onSubmit).toHaveBeenCalled();
});

Check if an onSubmitAndNavigate event was called:

it('check event on `Save & continue` button', () => {
    const streetNumberField = PropertySelfOwnedFormComponent.find('input[name="street_number"]');
    streetNumberField.simulate('change', { target: {value: '10'} });

    const propertySelfOwnedFormButton = PropertySelfOwnedFormComponent.find('button.button--secondary').at(0);
    propertySelfOwnedFormButton.simulate('click');
    expect(defaultProps.onSubmitAndNavigate).toHaveBeenCalled();
});

Check if an onNavigate event was called:

it('check event on `Next` button', () => {
    const propertySelfOwnedFormButton = PropertySelfOwnedFormComponent.find('button.button--secondary').at(1);
    propertySelfOwnedFormButton.simulate('click');
    expect(defaultProps.onNavigate).toHaveBeenCalled();
});

Full tests listing: PropertySelfOwnedFormIntegration.test.js

Now the form is fully tested, including the inside components being rendered.

To conclude, I’d like to say that both unit and integration testing are equally important. Each type of testing does its own work and has its own purpose. Dismissing one can lead to a lot of troubleshooting work in the future.

Unit testing mainly covers the user interface, while integration tests dig deeper into functionality. Some people find it redundant to do both, but I’d say both are necessary if you want your product to look good to the user, be user-friendly, and also work smoothly. Unit testing alone will never cover the most important part of the product – the interactions among components. Besides, it’s better to be safe than sorry.

When it comes to testing, forms require special attention, as forms are an essential part of many projects and a way to communicate with customers. Which is why it’s vital to be properly prepared and carefully go through all the stages – imports, mocks preparation, store creation, form decoration with Redux, and correct wrapper creation. But the tests themselves are not complicated. In most cases, they follow form logic and reflect field changes and button simulations (in the case of integration testing).

Thank you for your time. We look forward to reading your feedback!

This tutorial on unit and integration testing of React/Redux forms was written by Alyona Pysarenko — Frontend Developer at Django Stars.
Originally published on Django Stars blog.

Top comments (0)