The Right Way to Test React Components
There’s a lot of confusion right now about the “right” way to test your React components. Should you write all your tests by hand, or only use snapshots, or some of both? Should you test props? State? Styles/Layout?
I don’t think there’s one “right” way, but I’ve found a few patterns and tips that work really well for me that I’d like to share.
Update Feb 2019: This post describes a method for unit testing React components. Over time, I have found that I gain more value from integration tests than unit tests, so I personally no longer apply this method.
If you want to write integration tests for your React application, I highly recommend Cypress, which is what I now use.
However, you still may find this post useful - thinking about the method described in this post will help you write components with clearer contracts that are more reusable.
Background: The App We’ll Test
Suppose you want to test a LockScreen
component, which behaves like a phone’s lock screen. It:
- Shows the current time
- Can show a user-defined message
- Can show a user-defined background image
- Has a slide-to-unlock widget at the bottom
It looks something like this:
You can try it out here, and view the code on GitHub.
Here’s the code for the top-level App
component:
As you can see, LockScreen
receives three props: wallpaperPath
, userInfoMessage
, and onUnlocked
.
Here’s the code for LockScreen
:
LockScreen
pulls in a few other components, but since we’re only testing LockScreen
, let’s focus on it right now.
Component Contracts
In order to test LockScreen
, you must first understand what its Contract is. Understanding a component’s contract is the most important part of testing a React component. A contract defines the expected behavior of your component and what assumptions are reasonable to have about its usage. Without a clear contract, your component may be hard to understand. Writing tests is a great way to formally define your component’s contract.
Every React component has at least one thing that contributes to the definition of its contract:
- What it renders (which may be nothing)
Additionally, most component contracts are affected by these things as well:
- The props the component receives
- The state the component holds (if any)
- What the component does when the user interacts with it (via clicking, dragging, keyboard input, etc)
Some less common things that affect component contracts are:
- The context the component is rendered in
- What the component does when you call methods on its instance (public ref interface)
- Side effects that occur as part of the component lifecycle (componentDidMount, componentWillUnmount, etc)
To find your component’s contract, ask yourself questions like:
- What does my component render?
- Does my component render different things under different circumstances?
- When I pass a function as a prop, what does my component use it for?Does it call it, or just give it to another component? If it calls it, what does it call it with?
- When the user interacts with my component, what happens?
Finding LockScreen's Contract
Let’s go through LockScreen
’s render
method and add comments at places where its behavior can differ. You’ll look for ternaries, if statements, and switch statements as our clues. This will help us find variations in its contract.
We’ve learned three constraints that describe LockScreen
's contract:
- If a
wallpaperPath
prop is passed, the outermost wrappingdiv
that the component renders should have abackground-image
CSS property in its inline styles, set to whatever the value ofwallpaperPath
was, wrapped withinurl(...)
. - If a
userInfoMessage
prop is passed, it should be passed as children to aTopOverlay
, which should be rendered with a particular set of inline styles. - If a
userInfoMessage
prop is not passed, noTopOverlay
should be rendered.
You can also find some constraints of the contract that are always true:
- A
div
is always rendered, which contains everything else. It has a particular set of inline styles. - A
ClockDisplay
is always rendered. It does not receive any props. - A
SlideToUnlock
is always rendered. It receives the value of the passedonUnlocked
prop as itsonSlide
prop, regardless of if it was defined or not.
The component’s propTypes
are also a good place to look for clues about its contract. Here’s some more constraints I notice:
wallpaperPath
is expected to be a string, and is optional.userInfoMessage
is expected to be a string, and is optional.onUnlocked
is expected to be a function, and is optional.
This is a good starting point for our component contract. There may be more constraints within this component’s contract, and in production code you will want to find as many as you can, but for the purposes of this example, let’s just work with these. You can always add tests later if you discover additional constraints.
What’s Worth Testing?
Let’s look over the contract we found:
wallpaperPath
is expected to be a string, and is optional.userInfoMessage
is expected to be a string, and is optional.onUnlocked
is expected to be a function, and is optional.- A
div
is always rendered, which contains everything else. It has a particular set of inline styles. - A
ClockDisplay
is always rendered. It does not receive any props. - A
SlideToUnlock
is always rendered. It receives the value of the passedonUnlocked
prop as itsonSlide
prop, regardless of if it was defined or not. - If a
wallpaperPath
prop is passed, the outermost wrapping div that the component renders should have abackground-image
css property in its inline styles, set to whatever the value ofwallpaperPath
was, wrapped withinurl(...)
. - If a
userInfoMessage
prop is passed, it should be passed as children to aTopOverlay
, which should be rendered with a particular set of inline styles. - If a
userInfoMessage
prop is not passed, noTopOverlay
should be rendered.
Some of these constraints are worth testing, and others are not. Here are three rules of thumb I use to determine that something is not worth testing:
- Will the test have to duplicate exactly the application code? This will make it brittle.
- Will making assertions in the test duplicate any behavior that is already covered by (and the responsibility of) library code?
- From an outsider’s perspective, is this detail important, or is it only an internal concern? Can the effect of this internal detail be described using only the component’s public API?
These are only rules of thumb, so be careful not to use them to justify not testing something just because it’s hard. Often, things that seem hard to test are the most important to test, because the code under test is making many assumptions about the rest of the application.
Let’s go through our constraints and use these rules of thumb to determine which need to be tested. Here’s the first three:
wallpaperPath
is expected to be a string, and is optional.userInfoMessage
is expected to be a string, and is optional.onUnlocked
is expected to be a function, and is optional.
These constraints are a concern of React’s PropTypes
mechanism, and so writing tests around prop types fails rule #2 (already covered by library code). As such, I don’t test prop types. Because tests often double as documentation, I might decide to test something that failed rule #2 if the application code didn’t document the expected types very well, but propTypes
are already nice and human-readable.
Here’s the next constraint:
- A
div
is always rendered, which contains everything else. It has a particular set of inline styles.
This can be broken down into three constraints:
- A
div
is always rendered. - The rendered
div
contains everything else that gets rendered. - The rendered
div
has a particular set of inline styles.
The first two constraints that we broke this down into do not fail any of our rules of thumb, so we will test them. However, let’s look at the third one.
Ignoring the background-image property that is covered by another constraint, the wrapping div
has these styles:
height: "100%",
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
backgroundColor: "black",
backgroundPosition: "center",
backgroundSize: "cover",
If we wrote a test that these styles were on the div, we would have to test the value of each style exactly in order to make useful assertions. So our assertions might be something like:
- The wrapping div should have a height style property of 100%
- The wrapping div should have a display style property of flex
- …And so on for each style property
Even if we used something like toMatchObject
to keep this test succinct, this would duplicate the same styles in the application code, and be brittle. If we added another style, we would have to put the exact same code in our test. If we tweaked a style, we would have to tweak it in our test, even though the component’s behavior may not have changed. Therefore, this constraint fails rule #1 (duplicates application code; brittle). For this reason, I don’t test inline styles, unless they can change at runtime.
Often, if you are writing a test that amounts to “it does what it does”, or “it does exactly this, which happens to be duplicated in the application code”, then the test is either unnecessary or too broad.
Here’s the next two constraints:
- A
ClockDisplay
is always rendered. It does not receive any props. - A
SlideToUnlock
is always rendered. It receives the value of the passedonUnlocked
prop as itsonSlide
prop, regardless of if it was defined or not.
These can be broken down into:
- A
ClockDisplay
is always rendered. - The rendered
ClockDisplay
does not receive any props. - A
SlideToUnlock
is always rendered. - When the passed
onUnlocked
prop is defined, the renderedSlideToUnlock
receives that prop’s value as itsonSlide
prop. - When the passed
onUnlocked
prop isundefined
, the renderedSlideToUnlock
'sonSlide
prop should also be set toundefined
.
These constraints fall into two categories: “Some composite component is rendered”, and “the rendered component receives these props”. Both are very important to test, as they describe how your component interacts with other components. We will test all of these constraints.
The next constraint is:
- If a
wallpaperPath
prop is passed, the outermost wrapping div that the component renders should have abackground-image
css property in its inline styles, set to whatever the value ofwallpaperPath
was, wrapped withinurl(...)
.
You may think that, because this is an inline style, we do not need to test it. However, because the value of background-image
can change based on the wallpaperPath prop
, it needs to be tested. If we did not test it, then there would be no test around the effect of the wallpaperPath
prop, which is part of the public interface of this component. You should always test your public interface.
The final two constraints are:
- If a
userInfoMessage
prop is passed, it should be passed as children to aTopOverlay
, which should be rendered with a particular set of inline styles. - If a
userInfoMessage
prop is not passed, noTopOverlay
should be rendered.
These can be broken down into:
- If a
userInfoMessage
prop is passed, aTopOverlay
should be rendered. - If a
userInfoMessage
prop is passed, its value should be passed as children to the renderedTopOverlay.
- If a
userInfoMessage
prop is passed, the renderedTopOverlay
should be rendered with a particular set of inline styles. - If a
userInfoMessage
prop is not passed, noTopOverlay
should be rendered.
The first and fourth constraints (a TopOverlay
should/should not be rendered) describe what we render, so we will test them.
The second constraint verifies that the TopOverlay
receives a particular prop based on the value of userInfoMessage
. It is important to write tests around the props that rendered components receive, so we will test it.
The third constraint verifies that TopOverlay
receives a particular prop, so you might think that we should test it. However, this prop is just some inline styles. Asserting that props are passed is important, but making assertions about inline styles is brittle and duplicates application code (fails rule #1). Because it’s important to test passed props, it’s not clear whether this should be tested just by looking at rule #1 alone; luckily, that’s why I have rule #3. As a reminder, it’s:
From an outsider’s perspective, is this detail important, or is it only an internal concern? Can the effect of this internal detail be described using only the component’s public API?
When I write component tests, I only test the public API of the component (including side effects that API has on the application) where possible. The exact layout of this component is not impacted by this component’s public API; it is a concern of the CSS engine. Because of this, this constraint fails rule #3. Because it fails rule #1 and rule #3, we will not test this constraint, even though it verifies that TopOverlay
receives a prop, which is normally important.
It was hard to determine whether that final constraint should be tested or not. Ultimately, it is up to you to decide which parts are important to test; these rules of thumb I use are only guidelines.
Now we’ve gone through all of our constraints, and know which ones we are going to write tests for. Here they are:
- A
div
is always rendered. - The rendered
div
contains everything else that gets rendered. - A
ClockDisplay
is always rendered. - The rendered
ClockDisplay
does not receive any props. - A
SlideToUnlock
is always rendered. - When the passed
onUnlocked
prop is defined, the renderedSlideToUnlock
receives that prop’s value as itsonSlide
prop. - When the passed
onUnlocked
prop isundefined
, the renderedSlideToUnlock
'sonSlide
prop should also be set toundefined
. - If a
wallpaperPath
prop is passed, the outermost wrapping div that the component renders should have abackground-image
css property in its inline styles, set to whatever the value ofwallpaperPath
was, wrapped withinurl(...)
. - If a
userInfoMessage
prop is passed, aTopOverlay
should be rendered. - If a
userInfoMessage
prop is passed, its value should be passed as children to the renderedTopOverlay.
- If a
userInfoMessage
prop is not passed, noTopOverlay
should be rendered.
By examining our constraints and putting them to scrutiny, we broke many of them down into multiple, smaller constraints. This is great! This will make it easier to write our test code.
Setting Up Some Test Boilerplate
Let’s start scaffolding out a test for this component. I will be using Jest with enzyme in my tests. Jest works great with React and is also the test runner included in apps created with create-react-app, so you may already be set up to use it. Enzyme is a mature React testing library that works in both node and the browser.
Even though I’m using Jest and enzyme in my tests, you can apply the concepts here to almost any test configuration.
This is a lot of boilerplate. Let me explain what I’ve set up here:
- I create
let
bindings forprops
andmountedLockScreen
, so that those variables will be available to everything within thedescribe
function. - I create a
lockScreen
function that is available anywhere within thedescribe
function, that uses themountedLockScreen
variable to eithermount
aLockScreen
with the currentprops
or return the one that has already been mounted. This function returns an enzymeReactWrapper
. We will use it in every test. - I set up a
beforeEach
that resets theprops
andmountedLockScreen
variables before every test. Otherwise, state from one test would leak into another. By settingmountedLockScreen
toundefined
here, when the next test runs, if it callslockScreen
, a newLockScreen
will be mounted with the currentprops
.
This boilerplate may seem like a lot just to test a component, but it lets us build up our props incrementally before we mount our component, which will help keep our tests dry. I use it for all of my component tests, and I hope you will find it useful; its utility will become more apparent as we write the test cases.
Writing the Tests!
Let’s go through our list of constraints and add a test for each. Every test will be written such that it can be inserted at the // All tests will go here
comment in the boilerplate.
- A
div
is always rendered.
- The rendered
div
contains everything else that gets rendered.
- A
ClockDisplay
is always rendered.
- The rendered
ClockDisplay
does not receive any props.
- A
SlideToUnlock
is always rendered.
All of the constraints thus far have been things that are always true, so their tests were relatively simple to write. However, the remaining constraints begin with words like “If” and “When”. These are clues that they are conditionally true, and so we will pair describe
with beforeEach
to test them. This is where all that testing boilerplate we wrote earlier comes in handy.
- When the passed
onUnlocked
prop is defined, the renderedSlideToUnlock
receives that prop’s value as itsonSlide
prop. - When the passed
onUnlocked
prop isundefined
, the renderedSlideToUnlock
'sonSlide
prop should also be set toundefined
.
When we need to describe behavior that only occurs within a certain condition, we can describe
that condition, and then use beforeEach
within that describe
to set that condition up.
- If a
wallpaperPath
prop is passed, the outermost wrapping div that the component renders should have abackground-image
CSS property in its inline styles, set to whatever the value ofwallpaperPath
was, wrapped withinurl(...)
.
- If a
userInfoMessage
prop is passed, aTopOverlay
should be rendered. - If a
userInfoMessage
prop is passed, its value should be passed as children to the renderedTopOverlay.
- If a
userInfoMessage
prop is not passed, noTopOverlay
should be rendered.
That’s all of our constraints! You can view the final test file here.
“Not My Job”
When looking at the animated gif at the beginning of the article, you may have expected our test cases to end up as something like:
- When the user drags the slide-to-unlock handle all the way to the right, the unlock callback is called
- If the user drags the slide-to-unlock handle partway to the right and then releases it, the handle is animated back to its original position
- The clock at the top of the screen should always show the current time
This intuition is natural. From an application perspective, these are some of the most noticeable features.
However, we didn’t end up writing tests for any of that functionality. Why? They were not the concern of LockScreen
.
Because React components are reusable units, unit tests are a natural fit for them. And when unit testing, you should only test what your actual unit cares about. It is better to see the trees than the forest when writing React component tests.
Here is a handy cheat sheet that outlines the concerns of most React components:
- What do I do with the props I receive?
- What components do I render? What do I pass to those components?
- Do I ever keep anything in state? If so, do I invalidate it when receiving new props? When do I update state?
- If a user interacts with me or a child component calls a callback I passed to it, what do I do?
- Does anything happen when I’m mounted? When I’m unmounted?
The features described above are the concerns of SlideToUnlock
and ClockDisplay
, so tests around those features would go in the tests for those components, not here.
Summary
I hope these methods will help you write your own React component tests. To summarize:
- Find your Component Contract first
- Decide which constraints are worth testing and which aren’t
- Prop types are not worth testing
- Inline styles are usually not worth testing
- The components you render and what props you give them are important to test
- Don’t test things that are not the concern of your component
If you disagree or found this post helpful, I’d love to hear from you on twitter. Let’s all learn how to test React components together!
Although this article is licensed all rights reserved, all code samples in this article are available under the MIT license, as found in their source repository on GitHub.