Why I Never Use Shallow Rendering
Tests should help me be confident that my application is working and there are better ways to do that than shallow rendering.
Before we get into today’s newsletter I want to ask you to do a few things:
- Some people are saying that my newsletter is appearing in their spam/promotions/etc. in their inbox. You can help get this in the right place for people by adding postmaster@mg.buttondown.email to your contacts and moving this message to your priority inbox.
- I’m giving a workshop in Portland. There are only a few tickets left! Grab yours here.
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 React from 'react' import {CSSTransition} from 'react-transition-group' function Fade({children, ...props}) { return ( <csstransition classname="fade" timeout="{1000}" {...props}=""> {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 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}"></hiddenmessage>) 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 evaluates 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}"></hiddenmessage>) 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 though is that it’s way too heavy handed.
Not only does shallow rendering not render third party components, it doesn’t even render in-file components. For example, the <fade></fade>
component we have above is an implementation detail of the <hiddenmessage></hiddenmessage>
component, but because we’re shallow rendering <fade></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></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></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></csstransition>
properly (something the shallow rendering example was incapable of doing).
import 'react-testing-library/cleanup-after-each' import React from 'react' import {CSSTransition} from 'react-transition-group' import {render, fireEvent} from 'react-testing-library' 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', () => { const {getByText, queryByText} = render( <hiddenmessage initialshow="{true}"></hiddenmessage> ) const toggleButton = getByText('Toggle') 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(getByText('Hello world')).not.toBeNull() CSSTransition.mockClear() fireEvent.click(toggleButton) expect(queryByText('Hello world')).toBeNull() expect(CSSTransition).toHaveBeenCalledWith( {in: false, ...defaultProps}, context ) })
Conclusion
In today’s 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!
Looking for a job? Looking for a developer? Check out my job board: kcd.im/jobs There are two remote jobs and one job in Portland on there right now!
Learn more about testing from me:
- Frontend Masters:
- Testing Practices and Principles
- Testing React Applications
- Confidently Ship Production React Apps - Something new on egghead.io. It’s a recording of one of my talks especially for egghead.io. I think you’ll really enjoy it (and it’s š)
- Write tests. Not too many. Mostly integration. - My talk at Assert.js conference (and here’s the blog post)
- Testing Practices and Principles - A recording of my workshop at Assert.js
Things to not miss:
Some tweets from this last week:
> When I copy something to my clipboard, I get this odd sensation that I’m physically holding something in my (left) hand until I paste it where it needs to go. Am I alone in this? ā 7 Jul 2018
> Dah, every time I try to write my: “Never use shallow rendering” blog post I can’t get myself to do it. I just keep thinking about all the people who will yell at me for having an opinion that doesn’t align with their own and I just don’t want to deal with that… Sorry friends. ā 8 Jul 2018 (that’s this post!)
> <button type="primary">with primary color</button>
>
> <button type="accent">with accent color</button>
> “Make impossible states impossible” - lots of enlightened people. (I’ve heard it from @DavidKPiano and @rtfeldman) twitter.com/satya164/status/1015206655997472768 ā 6 Jul 2018
> Don’t forget to show love. twitter.com/owillis/status/992854500670140423 ā 5 Jul 2018
This week’s blog post is “What is JSX”. It’s the published version of my newsletter from 2 weeks ago. If you thought it was good, go ahead and give it some claps (šx50) and a retweet:
Special thanks to my sponsor Patreons: Hashnode
P.S. If you like this, make sure to subscribe, follow me on twitter, buy me lunch, support me on patreon, and share this with your friends š
š Hi! Iām Kent C. Dodds. I work at PayPal as a full stack JavaScript engineer. I represent PayPal on the TC39. Iām actively involved in the open source community. Iām an instructor on egghead.io, Frontend Masters, and Workshop.me. Iām also a Google Developer Expert. Iām happily married and the father of four kids. I like my family, code, JavaScript, and React.