Chris
Chris

Reputation: 3509

How to unit test a component under React Router 4

I have a component with behavior I would like to test:

import React from 'react';
import {connect} from 'react-redux';

import {getModules, setModulesFetching} from 'js/actions/modules';
import {setModuleSort} from 'js/actions/sort';

import Loading from 'js/Components/Loading/Loading';
import Module from './Components/ModuleListModule';

export class ModulesList extends React.Component {
    componentDidMount() {
        this.props.setModulesFetching(true);
        this.props.getModules();
    }

    renderModuleList() {
        if (this.props.isFetching) {
            return <Loading/>;
        }

        if (this._isSelected('name')) {
            this.props.modules.sort((a, b) => this._compareNames(a, b));
        } else if (this._isSelected('rating')) {
            this.props.modules.sort((a, b) => this._compareRatings(a, b));
        }

        return this.props.modules.map((module) =>
            <Module key={module.id} module={module}/>
        );
    }

    renderSelect() {
        return (
            <div className="col">
                <label htmlFor="search-sortby">Sort By:</label>
                <select id="search-sortby" onChange={(event) => this.props.setModuleSort(event.target.value)}>
                    <option value="name" selected={this._isSelected('name')}>Name</option>
                    <option value="rating" selected={this._isSelected('rating')}>Rating</option>
                </select>
            </div>
        );
    }

    render() {
        return (
            <div id="modules-list">
                <div className="p-3 row">
                    {this.renderSelect()}
                    <div id="search-summary">
                        {this.props.modules.length} Adventures Found
                    </div>
                </div>

                {this.renderModuleList()}
            </div>
        );
    }
// Other minor methods left out for brevity
}

The <Module/> subcomponent is interacting with React Router that's causing the trouble:

import React from 'react'
import {Link} from 'react-router-dom'

import ModuleStarRating from 'js/Components/ModuleRating/ModuleStarRating'

class ModuleListModule extends React.Component {
    // Once again, unrelated content omitted
    render() {
        return (
            <Link to={this.moduleLink()}>
                <div className="module">
                    <div className="p-2 row">
                        <div className="col">
                            <h5>{this.props.module.name}</h5>
                            <div className="module-subheader">
                                {this.props.module.edition.name} {this.renderLevel()} {this.renderLength()}
                            </div>
                            <div className="module-summary">{this.props.module.summary}</div>
                        </div>
                        <div className="col-2 text-center">
                            <ModuleStarRating current={this.currentRating()} readonly/>
                        </div>
                        <div className="col-2 text-center">
                            <img src={this.props.module.small_cover}/>
                        </div>
                    </div>
                </div>
            </Link>
        )
    }
}

According to the React Router docs, I should use a <MemoryRouter/> in my test to bypass the router functionality.

import {MemoryRouter} from 'react-router-dom';
import {ModulesList} from 'js/Scenes/Home/ModulesList';
import React from 'react';
import {assert} from 'chai';
import {shallow} from 'enzyme';
import sinon from 'sinon';

describe('ModulesList', () => {
    it('should sort by name, when requested.', () => {
        const initialProps = {
            getModules: sinon.stub(),
            isFetching: false,
            modules: [],
            setModulesFetching: sinon.stub(),
            sortBy: 'name'
        };

        const wrapper = shallow(
            <MemoryRouter>
                <ModulesList {...initialProps}/>
            </MemoryRouter>
        );
        const nextModules = [
            {
                avg_rating: [{
                    aggregate: 1.0
                }],
                edition: {
                    name: "fakeedition"
                },
                id: 0,
                name: "Z module"
            },
            {
                avg_rating: [{
                    aggregate: 1.0
                }],
                edition: {
                    name: "fakeedition"
                },
                id: 1,
                name: "Y module"
            }
        ];

        wrapper.setProps({modules: nextModules});
        assert.equal(wrapper.render().find('#search-summary').text(), '3 Adventures Found');
    });
});

However this test fails. Calling enzyme's ShallowWrapper.setProps() won't have the desired effect because the props aren't being applied to my component, they're applied to <MemoryRouter/>. But I can't omit <MemoryRouter/> because if I do, I get this error:

Warning: Failed context type: The context `router` is marked as required in `Link`, but its value is `undefined`.
in Link (created by ModuleListModule)
in ModuleListModule
in div

TypeError: Cannot read property 'history' of undefined

How can I setup my test such that I can call setProps() and the component actually updates?

Upvotes: 0

Views: 461

Answers (1)

Chris
Chris

Reputation: 3509

Worked on it for a few days and came up with a solution. I picked up a third party library to mock up the Redux store, but the rest I cobbled together from googling.

/* global describe, it */
import {MemoryRouter} from 'react-router-dom';
import {ModulesList} from 'js/Scenes/Home/ModulesList';
import {Provider} from 'react-redux';
import React from 'react';
import {assert} from 'chai';
import configureStore from 'redux-mock-store';
import {shallow} from 'enzyme';
import sinon from 'sinon';

describe('ModulesList' () => {
    it('should sort by name, when requested.', () => {
        const storeFactory = configureStore([]);
        const store = storeFactory({});
        # This is the harness my component needs in order to function in the
        # test environment.
        const TestModulesList = (props) => {
            return (
                <Provider store={store}>
                    <MemoryRouter>
                        <ModulesList {...props}/>
                    </MemoryRouter>
                </Provider>
            );
        };

        const initialProps = {
            getModules: sinon.stub(),
            isFetching: false,
            modules: [],
            setModulesFetching: sinon.stub(),
            sortBy: 'name'
        };

        const wrapper = shallow(<TestModulesList {...initialProps}/>);
        const nextModules = [/* omitted */];
        wrapper.setProps({modules: nextModules});

        assert.equal(wrapper.render().find('#search-summary').text(), '2 Adventures Found');
    });
});

Upvotes: 1

Related Questions