Easier React Component Testing through Dependency Injection HOCs
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:
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:
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:
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:
And here’s what FooComponent would look like in the render prop scenario:
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!