Why are my React Tests so Shallow?

Enzyme is a testing utility library for React. It lets you shallow render your components one level deep so you can test them in isolation. I'll show the problem with shallow rendering and what you should use instead when unit testing React components.

Imagine I'm working in an Agile team and I've taken a fresh ticket off the board. The user story says "As a visitor to the site, I want to see a list of people". Before you know it, I've built a People component that displays a list of names from the array of people passed in.

const People = ({people}) => (
  <ul>
    {people.map(person => <li>{person.name}</li>)}
  </ul>
);

Happy with the code, I unit test the component to ensure that future code changes don't break the user story. I test that, when I pass three people into the component, it returns an unordered list with three items. I can move onto the next ticket, confident in the knowledge that should the story ever break, my test will let me know.

describe('<People />', () => {
  it ('should render three list items', () => {
    const wrapper = shallow(
      <People people={[
        { name: 'Dominik' },
        { name: 'Etiene' },
        { name: 'Fabio' },
      ]} />
    );
    assert.equal(wrapper.find('ul li').length, 3);
  });
});

A Confidence Trick

The next ticket I pick up reads "As a visitor to the site, I want to see a list of each person's hobbies". With the previous ticket fresh in my mind, I see an opportunity for a refactor. Both tickets display lists, so I'll create a List component that I can share between the two stories. I refactor the People component, confident that my unit test will tell my if I break the first story.

const List = ({items, children}) => (
  <ul>
    {items.map(item => <li>{children(item)}</li>)}
  </ul>
);

const People = ({people}) => (
  <List items={people}>{person => person.name}</List>
);

I run the unit test and it fails. There must be something wrong with the refactor. I must've introduced a bug into the first story. I go back over the code looking for the problem, but I can't track it down. Is there anything wrong with the refactor or is the unit test lying?

What's the Story?

There's nothing wrong with the refactor. The problem is that the refactor changed the shallow rendering in the unit test. Instead of an unordered list with three items, it now returns a List component and so the assert fails. The test tells me that the People component's implementation has changed, but I only want to know when the story breaks.

If a unit test fails when the implementation changes then it's not refactor-friendly. Refactoring is fundamental to Agile. Because you only focus on the current ticket, each new ticket will likely cause a refactor. Shallow rendering is the enemy of refactoring because, when a test fails, you can't tell if you've broken a story. 

I can make my unit test refactor-friendly by using Enzyme's render function instead of shallow. This does a deep render. As long as the People component renders an unordered list with three items then the test passes. I'm now testing the story, not the implementation. The test will now only fail when the story breaks.

describe('<People />', () => {
  it ('should render three list items', () => {
    const wrapper = render(
      <People people={[
        { name: 'Fabio' },
        { name: 'Etiene' },
        { name: 'Dominik' },
      ]} />
    );
    assert.equal(wrapper.find('ul li').length, 3);
  });
});

Any questions, let me know @grahammendick

Get tickets for React London 2017!

Graham Mendick

Software Engineer

More from Graham