Writing (Unit) Testable React
The first time someone told me that unit testing should teach you how to design your code, I thought they meant for the backend only. As far as I can (anecdotally) tell, front end developers generally struggle with unit tests. I distinctly remember believing that writing React tests for real life products was hard because it took a lot of mocking user interaction to test what I wanted. While it’s true that we have to interact with React components to test some of the behavior of our front end apps, much of our code can be designed to become testable. I will also argue that the Redux/Reducer pattern solves much of our testing woes if we use it correctly, and not simply as a complex state setter/getter flow.
Get logic out of your components
Unit testing starts with creating units to test. If you have a large component with a lot of logic inside of it, you’re more likely to have a hard time testing it. Designing your components to get logic out of the component, and to return something meaningful that React can use in JSX or in state is a huge win. For starters, the function will no longer magically inherit values from other parts of your component in the body of the function — it forces us to pass these values as parameters instead which is less error prone. In terms of testability, we can pass these parameters in our tests and expect a result back. This is far simpler than trying to mock user interaction to dig into the logical branches of functions. Better yet, if we put the logic in a separate file, we can mock it to return different values when we test our component in isolation.
Use Redux as Intended
Redux has gotten a lot of hate lately. I’ve spoken to people who don’t like Redux that don’t fully understand how Redux can be used to manage complex apps. Many have treated Redux as a solution for sharing state among apps and that’s it. Redux is much more than that. For starters, Redux shifts our thinking from state setters/getters to events. The way I like to use Redux is to create a new action for each handler/event in my React component — even if something else in the component does something similar. This way there’s no confusion about what behavior I’m editing when I make updates to how my React component should treat events. With complex apps, this is big win.
In terms of testability, Redux helps solve this issue by creating space for small units of code to exist. Here’s how I believe each step of Redux should be used if you want to make the most of its value:
- Actions — Enumerate each action to represent a single event in your application
- Side Effect Managers (i.e. Thunk/Sagas)— Resolve all side effects here — even if they don’t have anything to give to your reducer. Create a space for side effects that are related to events via actions and are isolated from the rest of your application.
- Reducers — Put all your pure logic associated with an event here. If there’s something that would otherwise make your reducer code impure, resolve it in the side effect manager first then deal with the result of the side effect in a pure way in your reducer. Keep the logic out of your component and in your reducer, then return an updated version of your store.
- Store — Create state that represents the direct outcome of events, not state that represents how it impacts component (there’s a difference).
- Selectors — This is where we interpret state to by synthesizing (potentially) multiple state variables and adding logic to produce a return value to your component.
So what about testing? You can test Side Effect Managers, Reducers, AND selectors in isolation. You can also mock their results on their way in and out of your React components. Redux is amazing for testing when you use it like this, and also shifts our thinking to events rather than trying to understand the context of state setter/getter that we’re going to impact when making changes. Use Redux! (or should I say, use Redux in a useful way!)