React Higher Order Component Testing

Kyle Burke
3 min readJul 12, 2018

--

On the Ujo Team, we use higher order components to abstract and centralize logic away from components. It assists in the separation of concerns and promotes reusability. However, we were running into an issue when attempting to test them. To learn more about higher order components, start here.

In short, a higher order function is a function that returns a react component but is not a react component. We use Jest and Enzyme as our testing framework which provides us the ability to test internal methods and perform updates to state and props, functionality that we would not be possible if wrapped in a function. So the issue that needed solving was, “how can we test the component wrapped in a higher order function?”

Below is a contrived example of a higher order function that returns a connected component responsible for loading either a loading page or the loaded route once “stuff” has been loaded.

export default (LoadedRoute, needsType) => {
export class LoadingHOC extends React.Component {
static propTypes = {
needsType: PropTypes.string,
loadThatStuff: PropTypes.func.isRequired,
loadThisStuff: PropTypes.func.isRequired,
stuffLoaded: PropTypes.bool.isRequired,
}
static defaultProps = {
needsType: 'defaultValue',
}
componentDidMount() {
if (needsType === "that") this.props.loadThatStuff();
else this.props.loadThisStuff();
}
render() {
if (!this.props.stuffLoaded) return <LoadingPage />;
return <LoadedRoute {...this.props} />;
}
}
function mapStateToProps(state) {
return {
stuffLoaded: state.stuff.loaded,
};
}
return connect(
mapStateToProps,
{ loadThatStuff, loadThisStuff },
)(LoadingHOC);
};

This creates complications when attempting to test the functionality of the LoadingHOC. How could one shallow render the component and update it’s ‘stuffLoaded’ prop to see if it renders the proper component? What if I wanted to test an internal method on LoadingHOC?

After a couple hours of dead end solutions, the answer ended up being easier than I could’ve imagined. The solution was using React’s ‘extends’ functionality to decouple the inner higher order component from its setting in the function. By using extends, you can reuse the appropriate logic while overwriting any internal methods to make it’s use in the higher order context more complete. For instance, passing in function arguments as default props or rendering the proper route as opposed to a placeholder div.

export default (LoadedRoute, needsType) => {
export class LoadingHOC extends LoadingHOCLogic {
// pass in the defaultProp from the function argument
static defaultProps = { needsType }
render() {
if (!this.props.stuffLoaded) return <LoadingPage />;
return <LoadedRoute {...this.props} />;
}
}
function mapStateToProps(state) {
return {
stuffLoaded: state.stuff.loaded,
};
}
return connect(
mapStateToProps,
{ loadThatStuff, loadThisStuff },
)(LoadingHOC);
};
export class LoadingHOCLogic extends React.Component {
static propTypes = {
needsType: PropTypes.string,
loadThatStuff: PropTypes.func.isRequired,
loadThisStuff: PropTypes.func.isRequired,
stuffLoaded: PropTypes.bool.isRequired,
}
static defaultProps = {
needsType: 'that', // provide a default value
}
componentDidMount() {
if (needsType === "that") this.props.loadThatStuff();
else this.props.loadThisStuff();
}
render() {
if (!this.props.stuffLoaded) return <LoadingPage />;
return <div className="loaded-route" />;
}
}

Now we have abstracted the core logic outside of the higher order function providing a react component that is easily testable. There are two things to make note of here:

First, ‘needsType’ now becomes a prop which needs can be passed into the component. Notice in the high order function we just pass it in as the default prop. It enables us to do the following.

it('calls loadThisStuff if "this" is required', () => {
const thatFn = jest.fn();
const thisFn = jest.fn();
const hoc = shallow(
<LoadingHOCLogic
needsType='this'
loadThatStuff={thatFn}
loadThisStuff={thisFn}
stuffLoaded={false}
/>,
);
expect(thatFn.mock.calls.length).toEqual(0);
expect(thisFn.mock.calls.length).toEqual(1);
});

Second, we have to re-define the render function. This is so that we can test the render function and ‘LoadingPage’ is loaded. Just remember to update both render functions to reflect updates.

it('renders the loaded route once stuff is loaded', () => {
const thatFn = jest.fn();
const thisFn = jest.fn();
const hoc = shallow(
<LoadingHOCLogic
needsType='this'
loadThatStuff={thatFn}
loadThisStuff={thisFn}
stuffLoaded
/>,
);
const element = hoc.find('.loaded-route');
expect(element.length).toEqual(1);
});

Now you have a lovely component containing all the logic required to test as well as a slim higher order function. I hope this saves dev time for someone out there!

--

--