Why I Never Use Shallow Rendering

Photo by Teddy Kelley on Unsplash

Tests should help me be confident that my application is working and there are better ways to do that than shallow rendering.

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:

1 import React from 'react' 2 import { CSSTransition } from 'react-transition-group' 3 4 function Fade ( { children , ... props } ) { 5 return ( 6 < CSSTransition { ... props } timeout = { 1000 } className = " fade " > 7 { children } 8 </ CSSTransition > 9 ) 10 } 11 12 class HiddenMessage extends React . Component { 13 static defaultProps = { initialShow : false } 14 state = { show : this . props . initialShow } 15 toggle = ( ) => { 16 this . setState ( ( { show } ) => ( { show : ! show } ) ) 17 } 18 render ( ) { 19 return ( 20 < div > 21 < button onClick = { this . toggle } > Toggle </ button > 22 < Fade in = { this . state . show } > 23 < div > Hello world </ div > 24 </ Fade > 25 </ div > 26 ) 27 } 28 } 29 30 export { HiddenMessage }

Here's an example of a test that uses shallow rendering with enzyme:

1 import React from 'react' 2 import Enzyme , { shallow } from 'enzyme' 3 import Adapter from 'enzyme-adapter-react-16' 4 import { HiddenMessage } from '../hidden-message' 5 6 Enzyme . configure ( { adapter : new Adapter ( ) } ) 7 8 test ( 'shallow' , ( ) => { 9 const wrapper = shallow ( < HiddenMessage initialShow = { true } /> ) 10 expect ( wrapper . find ( 'Fade' ) . props ( ) ) . toEqual ( { 11 in : true , 12 children : < div > Hello world </ div > , 13 } ) 14 wrapper . find ( 'button' ) . simulate ( 'click' ) 15 expect ( wrapper . find ( 'Fade' ) . props ( ) ) . toEqual ( { 16 in : false , 17 children : < div > Hello world </ div > , 18 } ) 19 } )

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:

1 < div > 2 < button onClick = { [ Function ] } > Toggle </ button > 3 < Fade in = { true } > 4 < div > Hello world </ div > 5 </ Fade > 6 </ 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:

1 { 2 "type" : "div" , 3 "props" : { 4 "children" : [ 5 { 6 "type" : "button" , 7 "props" : { 8 "onClick" : [ Function ] , 9 "children" : "Toggle" 10 } 11 } , 12 { 13 "type" : [ Function : Fade ] , 14 "props" : { 15 "in" : true , 16 "children" : { 17 "type" : "div" , 18 "props" : { 19 "children" : "Hello world" 20 } 21 } 22 } 23 } 24 ] 25 } 26 }

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:

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?

1 test ( 'toggle toggles the state of show' , ( ) => { 2 const wrapper = shallow ( < HiddenMessage initialShow = { true } /> ) 3 expect ( wrapper . state ( ) . show ) . toBe ( true ) 4 wrapper . instance ( ) . toggle ( ) 5 wrapper . update ( ) 6 expect ( wrapper . state ( ) . show ) . toBe ( false ) 7 } )

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 the button to this.tgogle instead of this.toggle . My test continues to work, but my component is broken. I could rename toggle to handleButtonClick (and update the corresponding onClick 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:

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 its displayName (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).

1 import React from 'react' 2 import { CSSTransition } from 'react-transition-group' 3 import { render , screen , fireEvent } from '@testing-library/react' 4 import { HiddenMessage } from '../hidden-message' 5 6 7 8 9 jest . mock ( 'react-transition-group' , ( ) => { 10 return { 11 CSSTransition : jest . fn ( ( { children , in : show } ) => ( show ? children : null ) ) , 12 } 13 } ) 14 15 test ( 'you can mock things with jest.mock' , ( ) => { 16 render ( < HiddenMessage initialShow = { true } /> ) 17 const toggleButton = screen . getByText ( /toggle/i ) 18 19 const context = expect . any ( Object ) 20 const children = expect . any ( Object ) 21 const defaultProps = { children , timeout : 1000 , className : 'fade' } 22 23 expect ( CSSTransition ) . toHaveBeenCalledWith ( 24 { in : true , ... defaultProps } , 25 context , 26 ) 27 expect ( screen . getByText ( /hello world/i ) ) . not . toBeNull ( ) 28 29 CSSTransition . mockClear ( ) 30 31 fireEvent . click ( toggleButton ) 32 33 expect ( screen . queryByText ( /hello world/i ) ) . toBeNull ( ) 34 expect ( CSSTransition ) . toHaveBeenCalledWith ( 35 { in : false , ... defaultProps } , 36 context , 37 ) 38 } )

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!

Someone brought this up after I emailed my newsletter out:

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:

Effective Snapshot Testing

I hope that helps!