Shallow Rendering was introduced to React in 0.13. Its promise is that one can write tests without requiring a DOM. Sounds great! This article takes a closer look at shallow rendering, how it works, what can be tested with it and what cannot.

First of all, how do we use shallow rendering?

React comes with a bunch of test utilities called ReactTestUtils. One of their great features is shallow rendering. This is how it is done:

import ReactTestUtils from "react-addons-test-utils";

function shallowRender(component) {
  var renderer = ReactTestUtils.createRenderer();
  renderer.render(component);
  return renderer.getRenderOutput();
}

I’ve defined this as a helper function, it takes a component and returns the shallowly rendered result.

What can we get out of it?

Now, what can we get out of this result? Suppose we have a very simple (function) component

import React from "react";

export default () => <p>Simple Function Component</p>

This is what the shallowly rendered result looks like:

const result = shallowRender(<SimpleFunctionComponent />);
expect(JSON.stringify(result)).to.eql('{"type":"p","key":null,"ref":null,"props":{"children":"Simple Function Component"},"_owner":null,"_store":{}}');

As you can see, we basically get a JavaScript object that contains all kinds of useful information about what the component will display on screen when it gets rendered. (By the way, this object looks the same whether we render a function component or one that is derived from React.Component.)

We can examine individual parts of this object to make our tests more resilient. E.g. we can ask for the outermost element’s type or for its children:

expect(result.type).to.be('p');
expect(result.props.children).to.eql('Simple Class Component');

Nested Components

Of course, our components are not always that simple. We might also have a nested component, e.g.:

export default class extends Component {
  render() {
    return (
      <div>
        <p>Nested Component with</p>
        <SimpleFunctionComponent />
      </div>
    );
  }
}

We can also render it shallowly and examine the resulting object:

const result = shallowRender();

expect(JSON.stringify(result)).to.eql(
  '{"type":"div",' +
  '"key":null,' +
  '"ref":null,' +
  '"props":{"children":' +
  '[{"type":"p","key":null,"ref":null,"props":{"children":"Nested Component with"},"_owner":null,"_store":{}},' +
  '{"key":null,"ref":null,"props":{},"_owner":null,"_store":{}}]' +
  '},' +
  '"_owner":null,' +
  '"_store":{}}');

but this is not really that helpful any more. We can still ask more specific questions about the parts of this component:

expect(result.type).to.be('div');
expect(result.props.children.length).to.eql(2);

and we can even ask about its children:

expect(result.props.children[0].type).to.eql('p');

and about its children’s children (that’s because it is an HTML component):

expect(result.props.children[0].props.children).to.eql('Nested Component with');

Tests that follow this style become really long-winding and fragile because they are tightly coupled to the actual component layout. To improve this situation, many helper libraries for searching and accessing parts of the shallow rendering result have been developed, with AirBnB’s enzyme being one of the most well-known and most powerful ones. For the sake of simplicity - and because I want to show you the bare metal of shallow rendering in this article - we will not make use of its amazing search abilities in this article. But if you want to apply shallow rendering in your tests, by all means go and have a look at it.

Even when only using plain vanilla shallow rendering, we can still examine interesting aspects. First of all, we can observe that the second child component (the SimpleFunctionComponent) does not reveal anything interesting when it is rendered into a string:

expect(JSON.stringify(result.props.children[1])).to.eql('{"key":null,"ref":null,"props":{},"_owner":null,"_store":{}}');

That’s exactly why this method got its name: it only performs shallow rendering, i.e. it only renders our own components one level deep. The nested components are not rendered at all. In order to look at the contained components, we can render them shallowly, and then their contained components, and so on… So it’s shallow rendering all the way down.

Still, we can examine something about the child components:

import SimpleFunctionComponent from "../src/SimpleFunctionComponent";

// ...

expect(result.props.children[1].type).to.eql(SimpleFunctionComponent);

We can find out which type they have! (And please note that this is not a string comparison but that we check against the real subcomponent definition here.) This way, we can examine whether our components are constructed correctly.

Conditionally Rendered Subcomponents

This even applies to the dynamic aspects of hooking up the subcomponents. E.g. let’s look at a component that either includes a subcomponent or it doesn’t, depending on some property passed to it:

export default class extends Component {
  render() {
    return (
      <div>
        <p>Do we have a nested component?</p>
        { this.props.showIt ? <SimpleClassComponent /> : null }
      </div>
    );
  }
}

The shallowly rendered object does not contain the subcomponent if false is passed to the component:

import NotReallyNestedClassComponent from "../src/NotReallyNestedClassComponent";

describe('NotReallyNestedClassComponent', function () {
  it('checks the result\'s type and contents', function () {
    const result = shallowRender();

    expect(result.props.children.length).to.eql(2);
    expect(result.props.children[0].type).to.eql('p');

    expect(result.props.children[1]).to.eql(null);
  });
});

The component still has two children, and the first one still is a paragraph, but the second one gets rendered as null.

Passing Props to Subcomponents

Another aspect of the subcomponent we can examine is whether it was passed the correct props. Let’s modify our nested component a little bit:

import React, {Component} from "react";
import SimpleFunctionComponent from "./SimpleFunctionComponent";

export default class extends Component {
  render() {
    return (
      <div>
        <p>Nested Component passing prop with</p>
        <SimpleFunctionComponent passedProp={"Yes!"} />
      </div>
    );
  }
}

Now we can ask the shallow rendering result about the subcomponent’s props:

expect(result.props.children[1].props).to.eql({passedProp: "Yes!"});
// or
expect(result.props.children[1].props.passedProp).to.eql("Yes!");

These are the most interesting static aspects of React components, and we can both test them with shallow rendering, which is great. But what about some more dynamic aspects? How far does this approach lead us?

Callbacks

Let’s assume we have a component containing a button, and we are passing a handler function to this component via its props:

import React, {Component} from "react";

export default ({onButtonClick}) => (
  <div>
    <p>Button Component with</p>
    <button onClick={onButtonClick} >Click me!</button>
  </div>
);

Wouldn’t it be nice to be able to make sure that this handler function is actually wired correctly? We can indeed do that, but only if we resort to the magical powers of AirBnB’s enzyme that I already mentioned above. It comes with its own shallow rendering function which returns the shallow rendering result inside a wrapper object. This wrapper object allows us to actually simulate clicking the button on our shallowly rendered object:

import React from "react";
import sinon from "sinon";
import expect from "must";
import { shallow } from "enzyme";

import ButtonComponent from "../src/ButtonComponent";

describe('ButtonComponent', function () {

  it('simulates click events', () => {
    const handleButtonClick = sinon.spy();
    const wrapper = shallow(
      <ButtonComponent onButtonClick={handleButtonClick} />
    );

    wrapper.find('button').simulate('click');

    expect(handleButtonClick.calledOnce).to.equal(true);
  });
});

This is not possible (as far as I know) with plain vanilla shallow rendering because the button’s onClick prop is not included in the result object.

Changing Internal State and Internal Props

And there is even more! Let’s look at a checkbox component that reflects the checkbox status in the component’s internal state:

import React, {Component} from "react";

export default class extends Component {
  constructor(props) {
    super(props);
    this.state = {checked: true};
  }

  render() {
    return (
        <input type="checkbox"
               checked={this.state.checked}
               onChange={e => this.setState({checked: !this.state.checked}) }
        />
    );
  }
}

Using enzyme, we can observe that the internal state is modified when the checkbox is clicked:

it('can observe state changes', function () {
  const wrapper = shallow(<CheckboxComponentWithState />);

  expect(wrapper.state('checked')).to.eql(true);

  wrapper.simulate('change');

  expect(wrapper.state('checked')).to.eql(false);

  wrapper.simulate('change');

  expect(wrapper.state('checked')).to.eql(true);
});

and also that the component’s property is changed as well:

it('can observe internal property changes', function () {
  const wrapper = shallow(<CheckboxComponentWithState />);

  expect(wrapper.props().checked).to.eql(true);

  wrapper.simulate('change');

  expect(wrapper.props().checked).to.eql(false);

  wrapper.simulate('change');

  expect(wrapper.props().checked).to.eql(true);
});

Please note that those checks only work for the root component, not for any children of it. We’re only shallowly rendering our components, after all.

Lifecycle Methods

Another interesting category of tests deals with the React lifecycle methods. Most of those methods also get executed with enzyme’s shallow rendering. Let’s examine this more closely. This component implements many of the lifecycle methods and writes something to the console when they are invoked:

import React, {Component} from "react";

export default class extends Component {

  componentWillMount() {
    console.log("Component will mount.");
  }

  componentDidMount() {
    console.log("Component did mount.");
  }

  shouldComponentUpdate() {
    console.log("Should component update?");
    return true;
  }

  componentWillUpdate() {
    console.log("Component will update.");
  }

  componentDidUpdate() {
    console.log("Component did update.");
  }

  constructor(props) {
    super(props);
    this.state = {checked: true};
  }

  render() {
    return (
       { this.setState({checked: !this.state.checked}) }}
      />
    );
  }

}

We use another state-backed checkbox here because the state change triggers a component update. If we display this component on screen, we get this in the console:

Component will mount.
Component did mount.

And if we click the checkbox, we get this:

Should component update?
Component will update.
Component did update.

When we shallowly render the component by executing this test:

describe('LifecycleComponent', function () {
  it('executes most lifecycle methods', function () {
    console.log('Before rendering the component.');
    const wrapper = shallow();
    console.log('After rendering the component.');
    wrapper.simulate('change');
    console.log('After clicking the checkbox.');
  });
});

we get the following:

Before rendering the component.
Component will mount.
After rendering the component.
Should component update?
Component will update.
Component did update.
After clicking the checkbox.

So, as we can see, the only lifecycle method that did not get invoked is componentDidMount, which is understandable since the component was not mounted in the first place. (We are writing DOM-free tests here, remember?)

What can we not test on it?

There are of course cases that we cannot cover with shallow rendering:

As soon as we want to test something in integration (more than one level deep), we are at a loss. Also, as you’ve seen above, observing state and property changes is only possible for the root component. If you really want to test all state and property changes with shallow rendering, you will probably end up with a lot of very fine-grained components. This might be a good thing or a bad thing, I don’t want to judge here. You need to find out which level of granularity is good for you to work with.

Do you know of an example where shallow rendering is not sufficient to test your component? If so, I’d love to hear about it!

Summary

What we can test with shallow rendering:

  1. wiring of subcomponents into the component
  2. passing of subcomponent props
  3. wiring of handler functions that are passed to buttons etc.
  4. changes of state of the root component
  5. changes of properties of the root component
  6. the effects of most of the lifecycle methods

Repository

If you are interested in playing around with the code and tests that I showed here, you can find them online in this repository.


Comments:

(please comment on this article via email)