At Opendoor, we emphasize rigorous unit testing of all of our React components. Testing is often overlooked in frontend, perhaps due to its convolution or lack of clear best practices. However, having a strong testing philosophy provides many benefits:
- Facilitates discovering bugs in the development phase, before it reaches users
- Enforces writing better code that is more modular, covers edge cases, and easily testable
- Lowers the risk when making large changes or refactors
- Provides documentation and helps the next engineer understand what the code should do
Testing React doesn’t have to be difficult. In this article, we’ll discuss the tools we use and the best practices that we follow.
- Enzyme: React-specific testing library, allows you to manipulate and render components, access props and state, and more
- Coveralls: keeps track of code coverage for the repository
The following directory structure is how we organize our tests and components.
This follows Jest’s recommendation and default settings, and the consistency and proximity of the related files to each other keeps tests easy to find and maintain.
Here are a few Jest commands that might come in handy.
Enzyme provides the ability to “shallow” render a component — only rendering the content of the tested component and not of any child components. This is useful to test the component as an isolated unit and prevent the behavior of its child components from affecting the test. We should always use shallow rendering in our tests (of course, exceptions apply) as we don’t want one change in a child component to cause a cascading set of test failures unrelated to the change.
For the rest of this article, we will use a simple
BaseButton component from our UI library as our example. It wraps the native HTML
button element to provide styling and some additional functionality.
Jest’s snapshot testing functionality is useful to make sure the UI does not change unexpectedly. In the first run of a snapshot test, it creates a serialized version of the rendered React tree and stores that snapshot with the test. In subsequent runs, Jest is able to compare the newly rendered component to the stored snapshot to alert you of any differences.
This type of tests can be powerful because of its simplicity and ease of use. It’s a quick and simple way to capture expected render output, but we cannot depend on it exclusively. Snapshots can be dozens of lines long and are easily, mistakenly overwritten if the writer or the code reviewer does not carefully check the differences between the old and new snapshot. This can result in an incorrect snapshot being committed into the codebase, and future runs of the test may pass but are actually wrong. Additionally, snapshot tests do not specify the specific detail that it is trying to test, so it becomes difficult to maintain as multiple people start contributing to a component or as the component becomes more complex.
Despite some drawbacks, we typically have one snapshot test for every component to ensure that it renders as expected.
Tip: We use the
enzyme-to-json package as the snapshot serializer in our Jest config. This automatically converts the shallow render into a much more human-readable format for the snapshot file and removes extra cruft from the Enzyme shallow wrapper that we don’t care about.
enzyme-to-json serialized snapshots (much nicer!):
When not to use snapshot testing
For component behavior, we avoid snapshot tests if possible, and, instead, assert directly for the specific property, state, or element that we’re looking for. This clarifies the intention to future engineers on the team and makes the test more robust. If an unrelated part of the component render tree changes, it will not cause our specific check to fail.
Jest provides the functionality to create mock functions using
jest.fn() and also to specify its implementation if necessary. Mock functions allow us to assert whether the mocked function was called and, if so, with what arguments. This can be really powerful when combined with Enzyme to test the effect of certain user actions, leading us to the next section.
Interacting with components
In many cases, we want to test that the component’s behavior after a user performs an action, e.g. enters text into an input or presses a button, matches what we expect. A common pattern is to 1) use Enzyme to select the element of interest (e.g. the button), 2) simulate the event (e.g. a click event), and then 3) use Jest to check that the mocked function is called as expected.
Testing components connected to the Redux store
Many of our components are connected to the Redux store.
connect takes a component and returns a new, connected component that has any relevant values from the Redux store passed in as
props. This new, connected component is generally the default export for components in our repo, since it is the one that would be imported and used by other parts of the app.
When testing these components, we actually export the unconnected component as well. We pass in mock values as
props directly to the unconnected component, which allows us to better test the component in isolation. If we were to test the connected component, our test would expand to cover the Redux actions / selectors and the connection itself, which is not desirable. Our testing philosophy is that we should test each piece individually (component, Redux actions, etc.) in the JS, and test the end-to-end connections via separate integration tests.
For this example, suppose we want to modify our button component to display a listing price from our Redux store.
What tests should you include for each component?
Now that we know about a few different testing techniques and tools, what tests should we actually include for each component?
We typically begin with one snapshot test. This roughly checks that our component renders as we expect it to. Afterwards, we need enough tests to comprehensively test the external interface and behavior of our component in isolation. The external interface can include how the user interacts with the component, as well as how the component interacts or triggers external effects.
Here are some things to consider:
- Does the component behave differently when it receives different values for a certain prop?
- Is a specific element rendered in the tree when the state changes to a certain value?
- What happens after the user performs some action? Are any external functions called? Do we expect the state to change?
Now that we have some tests, it’s important to maintain and improve test coverage over time. We set up CircleCI to send the Jest coverage output to Coveralls on every pull request. This helps us ensure the overall test coverage for our codebase does not decrease as we build more features. The Coveralls dashboard also provides the ability to see overall test coverage for our repository over time.
At Opendoor, we also use Jest to test our React Native apps, and all the same best practices for React apply for React Native as well. It pretty much works out of the box — you just need to make one little change to the Jest config.
Testing React (and React Native) components is very valuable and not that difficult. With some investment, it can greatly increase the stability and quality of the frontend codebase. Here are some quick rules of thumb to help you get testing underway!
- Each and every component should have its own corresponding test file — If the component isn’t very complex, it’s okay to have one simple
it('renders correctly')snapshot test, but we should still test that.
- Each component should be restricted to being a unit test, and other dependencies or helper functions should be mocked out — Each unit of the codebase should be strongly tested individually, in isolation, and separate integration tests should be written to test how everything connects together. We should test a component via its public interface if possible.
- Always use shallow rendering. Deep rendering allows child components to have unintended side effects on a component unit test.