Easier React Component Testing through Dependency Injection HOCs

Ashwin Bhat
Hey Koan
Published in
4 min readMay 23, 2018
photo credit: Nathan Dumlao

At Koan, we have a large React SPA that makes heavy use of Redux selectors. These make it easy to get data from our Redux store in the exact derived format we need, but they require a populated store in order to function. This is fine for production, but when writing component unit tests, we generally don’t want so much preamble just to test our component’s functionality.

To address this, we’re using a pattern in our code that’s reminiscent of dependency injection frameworks in languages like Java, with promising results.

The Problem

Many of our components look something like this:

A mostly straightforward React component connected to a Redux store.

Here, we’ve gone through the effort of making our component a stateless function, so we know that part will be easy to test in isolation. But what about testing mapStateToProps? FooComponent may be dead simple, but now we’ve just offloaded the complexity to another function. And while it looks simple enough, there’s a problem: that call to fooSelector.

Given only the inputs (nextReduxState and nextOwnProps), we can’t deterministically say what the output of the function will be, meaning it’s not a pure function.

So how do we address this? One way might be to use a prewritten mock for the selector module. This could work, but it doesn’t actually make our function pure. In addition to that, it can be a bit opaque. So how do we make use of this selector while still having a pure function that’s clear to unit test?

The Dependency Injection Higher-Order Component

The solution we ended up with makes use of everyone’s favorite React abstraction, the Higher-Order Component (HOC), and its best friend, the HOC utility library Recompose. We’ll first create an HOC that just simply takes in a component and returns that same component, but with a fooSelector prop:

A higher-order component for injecting `fooSelector` as a prop into a component.

Here, we’re using withProps to inject the fooSelector module as a prop to the component, while keeping the rest of its props intact.

Now we’ll rewrite our original component with this new HOC:

Our component, but with `fooSelector` passed in from above.

Here, we’ve used compose to chain our HOCs in such a way that mapStateToProps gets to make use of fooSelector without needing to touch anything other than its own arguments! This is an idea borrowed from dependency injection (DI) frameworks in languages like Java. Now, our unit test for it might look something like:

Why not use a render prop?

When thinking about this, we considered whether an HOC was really the right approach. After all, render props seem to gaining steam in the React community, and using one would let us avoid the use of Recompose as well as having to wrap multiple layers of HOCs manually. So why did we still choose the HOC route? Let’s take a look at what a render prop version of withFooSelector might look like:

A render prop implementation of `fooSelector` dependency injection

And here’s what FooComponent would look like in the render prop scenario:

Our original component, but with our render prop component providing `fooSelector`

So what’s changed here? Well, instead of an HOC, we now have these WithFooSelector and WrappedFoo components that seem to be doing what withFooSelector was doing, only now we have both a render prop component and the instantiation of it to deal with. With the HOC approach, we only needed one wrapper component and we were done. This is a pretty minor tradeoff, so it seems like render props might have been a fine choice here, but we chose to avoid having to create a new Wrapped* component for every case where we needed fooSelector injected. And while this extra wrapping could be abstracted away to reduce boilerplate, we’d likely end up in more or less the same place as we were with the HOC.

HOCs as Dependency Injection

With this approach, we’ve shown that without any real DI framework, we can get many of the benefits of DI, including dependency swap-ability and unit testability. If we had a second fooSelector that we wanted to use with our component in a different part of the app, we could easily do so without modifying any of the internals of our component, and certainly without adding any hairy conditional logic! We also were able to turn more of our core application logic into pure functions, making writing tests a breeze.

One downside is that we now have a layer of indirection in our code that could hurt readability: before, a reader could see the exact module that fooSelector was coming from without any ambiguity. Now, the reader needs to look at the implementation of withFooSelector to see exactly where fooSelector is coming from. So far, this hasn’t manifested as a real pain point for us during development, but we’ll continue to evaluate our assumptions as we use this pattern more.

We’re also now exposing ourselves to the risk of our mocked version of fooSelector in our unit test drifting from the real implementation in modules/selectors/fooSelector. This is a common risk of mocking in unit tests, and one that wouldn’t be truly solved even with Jest’s manual mocks. One approach we may go with to lower this risk is to use fakes rather than mocks: this would involve us maintaining a single fake implementation of fooSelector in our repo, and injecting that into components in our tests. While we’d still be exposed to the risk of the fake drifting from the real implementation, we’d have consolidated that risk into a single module in our repo.

So is DI through HOCs a worthwhile abstraction? We think it’s promising enough to continue exploring, and we’re optimistic that we can work to minimize the shortcomings while still preserving purity and testability in our code. We’ll report back here with the results!

--

--

Ashwin Bhat
Hey Koan

Senior Software Engineer at Koan (https://koan.co/). Previously at Airbnb and Facebook.