I remember a few years ago when I got started with React I decided I needed to
figure out how to test React components. I tried
shallow
from enzyme and
immediately decided that I would never use it to test my React components. I've
expressed this feeling on many occasions and get asked on a regular basis why I
feel the way I do about shallow
rendering and why
React Testing Library
will never support shallow
rendering.
So finally I'm coming out with it and explaining why I never use shallow rendering and why I think nobody else should either. Here's my main assertion:
With shallow rendering, I can refactor my component's implementation and my tests break. With shallow rendering, I can break my application and my tests say everything's still working.
This is highly concerning to me because not only does it make testing frustrating, but it also lulls you into a false sense of security. The reason I write tests is to be confident that my application works and there are far better ways to do that than shallow rendering.
What even is shallow rendering?
For the purposes of this article, let's use this example as our subject under test:
import * as React from 'react'
import { CSSTransition } from 'react-transition-group'
function Fade({ children, ...props }) {
return (
<CSSTransition {...props} timeout={1000} className="fade">
{children}
</CSSTransition>
)
}
class HiddenMessage extends React.Component {
static defaultProps = { initialShow: false }
state = { show: this.props.initialShow }
toggle = () => {
this.setState(({ show }) => ({ show: !show }))
}
render() {
return (
<div>
<button onClick={this.toggle}>Toggle</button>
<Fade in={this.state.show}>
<div>Hello world</div>
</Fade>
</div>
)
}
}
export { HiddenMessage }
Here's an example of a test that uses shallow rendering with enzyme:
import * as React from 'react'
import Enzyme, { shallow } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import { HiddenMessage } from '../hidden-message'
Enzyme.configure({ adapter: new Adapter() })
test('shallow', () => {
const wrapper = shallow(<HiddenMessage initialShow={true} />)
expect(wrapper.find('Fade').props()).toEqual({
in: true,
children: <div>Hello world</div>,
})
wrapper.find('button').simulate('click')
expect(wrapper.find('Fade').props()).toEqual({
in: false,
children: <div>Hello world</div>,
})
})
To understand shallow rendering, let's add a console.log(wrapper.debug())
which will log out the structure of what enzyme has rendered for us:
<div>
<button onClick={[Function]}>Toggle</button>
<Fade in={true}>
<div>Hello world</div>
</Fade>
</div>
You'll notice that it's not actually showing the CSSTransition
which is what
Fade
is rendering. This is because instead of actually rendering components
and calling into the Fade
component, shallow just looks at the props that
would be applied to the React elements created by the component you're shallowly
rendering. In fact, if I were to take the render
method of the HiddenMessage
component and console.log
what it's returning, I'd get something that looks a
bit like this:
{
"type": "div",
"props": {
"children": [
{
"type": "button",
"props": {
"onClick": [Function],
"children": "Toggle"
}
},
{
"type": [Function: Fade],
"props": {
"in": true,
"children": {
"type": "div",
"props": {
"children": "Hello world"
}
}
}
}
]
}
}
Look familiar? So all shallow rendering is doing is taking the result of the
given component's render
method (which will be a React element (read
What is JSX?)) and giving us a wrapper
object with some
utilities for traversing this JavaScript object. This means it doesn't run
lifecycle methods (because we just have the React elements to deal with), it
doesn't allow you to actually interact with DOM elements (because nothing's
actually rendered), and it doesn't actually attempt to get the react elements
that are returned by your custom components (like our Fade
component).
Why people use shallow rendering
When I determined early on to never use shallow rendering, it was because I knew that there were better ways to get at the things that shallow rendering makes easy without making the trade-offs shallow rendering forces you to make. I recently asked folks to tell me why they use shallow rendering. Here are a few of the things that shallow rendering makes easier:
- ... for calling methods in React components
- ... it seems like a waste to render all of the children of each component under test, for every test, hundreds/thousands of times...
- For actual unit testing. Testing composed components introduces new dependencies that might trigger an error while the unit itself might still work as intended.
There were more responses, but these sum up the main arguments for using shallow rendering. Let's address each of these:
Calling methods in react components
Have you ever seen or written a test that looks like this?
test('toggle toggles the state of show', () => {
const wrapper = shallow(<HiddenMessage initialShow={true} />)
expect(wrapper.state().show).toBe(true) // initialized properly
wrapper.instance().toggle()
wrapper.update()
expect(wrapper.state().show).toBe(false) // toggled
})
This is a great reason to use shallow rendering, but it's a really poor testing practice. There are two really important things that I try to consider when testing:
- Will this test break when there's a mistake that would break the component in production?
- Will this test continue to work when there's a fully backward compatible refactor of the component?
This kind of test fails both of those considerations:
- I could mistakenly set
onClick
of thebutton
tothis.tgogle
instead ofthis.toggle
. My test continues to work, but my component is broken. - I could rename
toggle
tohandleButtonClick
(and update the correspondingonClick
reference). My test breaks despite this being a refactor.
The reason this kind of test fails those considerations is because it's testing
irrelevant implementation details. The user doesn't care one bit what things are
called. In fact, that test doesn't even verify that the message is hidden
properly when the show
state is false
or shown when the show
state is
true
. So not only does the test not do a great job keeping us safe from
breakages, it's also flakey and doesn't actually test the reason the component
exists in the first place.
In summary, if your test uses instance()
or state()
, know that you're
testing things that the user couldn't possibly know about or even care about,
which will take your tests further from giving you confidence that things will
work when your user uses them.
... it seems like a waste ...
There's no getting around the fact that shallow rendering is faster than any other form of testing react components. It's certainly way faster than mounting a react component. But we're talking a handful of milliseconds here. Yes, it will add up, but I'd gladly wait an extra few seconds or minutes for my tests to finish in exchange for my tests actually giving me confidence that my application will work when I ship it to users.
In addition to this, you should probably use Jest's capabilities for only running tests relevant to your changes while developing your tests so the difference wont be perceivable when running the test suite locally.
For actual unit testing
This is a very common misconception: "To unit test a react component you must use shallow rendering so other components are not rendered." It's true that shallow rendering doesn't render other components (as demonstrated above), what's wrong with this is that it's way too heavy handed because it doesn't render any other components. You don't get a choice.
Not only does shallow rendering not render third party components, it doesn't
even render in-file components. For example, the <Fade />
component we have
above is an implementation detail of the <HiddenMessage />
component, but
because we're shallow rendering <Fade />
isn't rendered so changes to that
component could break our application but not our test. That's a major issue in
my mind and is evidence to me that we're testing implementation details.
In addition, you can definitely unit test react components without shallow
rendering. Checkout the section near the end for an example of such a test (uses
React Testing Library, but you could do this with enzyme as well) that uses Jest
mocking to mock out the <CSSTransition />
component.
I should add that I generally am against mocking even third party components 100% of the time. The argument for mocking third party components I often hear is Testing composed components introduces new dependencies that might trigger an error while the unit itself might still work as intended.. But isn't the point of testing to be confident the application works? Who cares if your unit works if the app is broken? I definitely want to know if the third party component I'm using breaks my use case. I mean, I'm not going to rewrite their entire test base, but if I can easily test my use case by not mocking out their component then why not do that and get the extra confidence?
I should also add that I'm in favor of relying more heavily on integration testing. When you do this, you need to unit test fewer of your simple components and wind up only having to unit test edge cases for components (which can mock all they want). But even in these situations, I still think it leads to more confidence and a more maintainable testbase when you're explicit about which components are being mocked and which are being rendered by doing full mounting and explicit mocks.
Without shallow rendering
I'm a huge believer of the guiding principle of React Testing Library:
The more your tests resemble the way your software is used, the more confidence they can give you. — Kent C. Dodds 👋
That's why I wrote the library in the first place. As a side-note to this shallow rendering post, I want to mention there are fewer ways for you to do things that are impossible for the user to do. Here's the list of things that React Testing Library cannot do (out of the box):
- shallow rendering
- Static rendering (like enzyme's
render
function). - Pretty much most of enzyme's methods to query elements (like
find
) which include the ability to find by a component class or even itsdisplayName
(again, the user does not care what your component is called and neither should your test). Note: React Testing Library supports querying for elements in ways that encourage accessibility in your components and more maintainable tests. - Getting a component instance (like enzyme's
instance
) - Getting and setting a component's props (
props()
) - Getting and setting a component's state (
state()
)
All of these things are things which users of your component cannot do, so your
tests shouldn't do them either. Below is a test of the <HiddenMessage />
component which resembles the way a user would use your component much more
closely. In addition, it can verify that you're using <CSSTransition />
properly (something the shallow rendering example was incapable of doing).
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { CSSTransition } from 'react-transition-group'
import { HiddenMessage } from '../hidden-message'
// NOTE: you do NOT have to do this in every test.
// Learn more about Jest's __mocks__ directory:
// https://jestjs.io/docs/en/manual-mocks
jest.mock('react-transition-group', () => {
return {
CSSTransition: jest.fn(({ children, in: show }) =>
show ? children : null,
),
}
})
test('you can mock things with jest.mock', () => {
render(<HiddenMessage initialShow={true} />)
const toggleButton = screen.getByText(/toggle/i)
const context = expect.any(Object)
const children = expect.any(Object)
const defaultProps = { children, timeout: 1000, className: 'fade' }
expect(CSSTransition).toHaveBeenCalledWith(
{ in: true, ...defaultProps },
context,
)
expect(screen.getByText(/hello world/i)).not.toBeNull()
CSSTransition.mockClear()
userEvent.click(toggleButton)
expect(screen.queryByText(/hello world/i)).toBeNull()
expect(CSSTransition).toHaveBeenCalledWith(
{ in: false, ...defaultProps },
context,
)
})
Conclusion
A few weeks ago, my DevTipsWithKent (my weekdaily livestream on YouTube) I livestreamed "Migrating from shallow rendering react components to explicit component mocks". In that I demonstrate some of the pitfalls of shallow rendering I describe above as well as how to use jest mocking instead.
I hope this is helpful! We're all just trying our best to deliver an awesome experience to users. I wish you luck in that endeavor!
P.S.
Someone brought this up:
Shallow wrapper is good to test small independent components. With proper serializer it allows to have clear and understandable snapshots.
I very rarely use snapshot testing with react and I certainly wouldn't use it with shallow. That's a recipe for implementation details. The whole snapshot is nothing but implementation details (it's full of component and prop names that change all the time on refactors). It'll fail any time you touch the component and the git diff for the snapshot will look almost identical to the one for your changes to the component.
This will make people careless about changes to the snapshot updates because they change all the time. So it's basically worthless (almost worse than no tests because it makes you think you're covered when you're not and you won't write proper tests because they're in place).
I do think that snapshots can be useful though. For more about this from me, checkout another blog post:
I hope that helps!