Developing React.js applications using a test-driven approach, utilizing Enzyme and Jest for unit testing

It is commonly accepted among developers, following Michael Feathers’ definition, that code without tests can be categorized as legacy code. To prevent accumulating such legacy code, the test-driven development (TDD) approach offers a robust solution.

Although JavaScript and React.js have numerous tools for unit testing, this article will demonstrate how to build a basic React.js component using Jest and Enzyme, employing a TDD methodology.

The Advantages of TDD for React.js Component Creation

TDD offers numerous advantages for your codebase. High test coverage, a direct result of TDD, simplifies code refactoring while maintaining code clarity and functionality.

Creating React.js components often leads to rapid code expansion, becoming intricate due to state changes and service calls.

Components without unit tests become difficult-to-maintain legacy code. While adding tests after production code is possible, it risks overlooking testable scenarios. Creating tests first increases the likelihood of covering every logic branch in a component, simplifying refactoring and maintenance.

Strategies for Unit Testing React.js Components

Several strategies prove effective for testing a React.js component:

  • Verifying that a specific props function is called when a particular event is dispatched.
  • Comparing the render function’s output, based on the current component state, to a predefined layout.
  • Checking if the number of child components aligns with an expected value.

To implement these strategies, we will utilize two tools renowned for their effectiveness in React.js testing: Jest and Enzyme.

Jest: A Powerful Tool for Unit Test Creation

Jest, an open-source test framework developed by Facebook, boasts excellent integration with React.js. It offers a command-line test execution tool similar to Jasmine and Mocha, simplifies mock function creation with minimal configuration, and provides a clear set of matchers for readable assertions.

Furthermore, Jest’s “snapshot testing” functionality proves invaluable for verifying component rendering results. We’ll use snapshot testing to capture and save a component’s tree structure. This saved snapshot can then be compared against subsequent rendering trees or any structure passed as the first argument to the expect function.

Enzyme: Facilitating React.js Component Mounting and Traversal

Enzyme provides a mechanism for mounting and traversing React.js component trees. This access allows us to interact with component properties, state, and children’s props for assertion purposes.

Enzyme offers two primary functions for component mounting: shallow and mount. While shallow loads only the root component into memory, mount constructs the complete DOM tree.

We will combine Enzyme and Jest to mount React.js components and execute assertions.

TDD steps to create a react component

Setting Up the Development Environment

For this example, refer to this repo for the basic configuration.

We’ll be using these versions:

1
2
3
4
5
6
7
{
  "react": "16.0.0",
  "enzyme": "^2.9.1",
  "jest": "^21.2.1",
  "jest-cli": "^21.2.1",
  "babel-jest": "^21.2.0"
}

Building a React.js Component Using TDD

Our first step is to create a failing test that attempts to render a React.js Component using Enzyme’s shallow function.

1
2
3
4
5
6
7
8
9
// MyComponent.test.js
import React from 'react';
import { shallow } from 'enzyme';
import MyComponent from './MyComponent';
describe("MyComponent", () => {
  it("should render my component", () => {
    const wrapper = shallow(<MyComponent />);
  });
});

Running this test results in the following error:

1
ReferenceError: MyComponent is not defined.

Next, we create the component with the minimal syntax necessary to make the test pass.

1
2
3
4
5
6
7
8
// MyComponent.js
import React from 'react';

export default class MyComponent extends React.Component {
  render() {
    return <div />;
  }
}

Moving on, we’ll ensure our component renders a predefined UI layout using Jest’s toMatchSnapshot function.

This method generates a snapshot file named [testFileName].snap in the __snapshots__ folder. This file represents the expected UI layout resulting from our component rendering.

However, adhering to pure TDD principles requires creating this snapshot file before calling the toMatchSnapshot function, intentionally causing the test to fail.

This might seem counterintuitive since we don’t initially know the format Jest uses for this layout.

It’s tempting to execute toMatchSnapshot first to observe the generated snapshot file, and that’s certainly an option. However, for true pure TDD, we must understand the structure of snapshot files.

The snapshot file mirrors the test’s name in its layout. For example, a test structured like this:

1
2
3
4
5
desc("ComponentA" () => {
  it("should do something", () => {
    
  }
});

Should have a corresponding entry in the exports section: Component A should do something 1.

For a more detailed understanding of snapshot testing, refer to here.

Therefore, we begin by creating the MyComponent.test.js.snap file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//__snapshots__/MyComponent.test.js.snap
exports[`MyComponent should render initial layout 1`] = `
Array [
<div>
    <input
         type="text"
    />
</div>,
]
`;

Next, we create the unit test to verify that the snapshot matches the component’s child elements.

1
2
3
4
5
6
7
8
9
// MyComponent.test.js
...
it("should render initial layout", () => {
    // when
    const component = shallow(<MyComponent />);
    // then
    expect(component.getElements()).toMatchSnapshot();
});
...

We can consider components.getElements as the output of the render method.

These elements are passed to the expect method for verification against the snapshot file.

Upon test execution, we encounter the following error:

1
2
3
4
5
6
7
8
9
Received value does not match stored snapshot 1.
Expected:
 - Array [
    <div>
        <input type="text” />
     </div>,
    ]
Actual:
+ Array []

Jest indicates that the output from component.getElements does not correspond to the snapshot. We address this by incorporating the input element into MyComponent.

1
2
3
4
5
6
7
// MyComponent.js
import React from 'react';
export default class MyComponent extends React.Component {
  render() {
    return <div><input type="text" /></div>;
  }
}

Now, we enhance the input functionality by executing a function when its value changes. This is accomplished by defining a function within the onChange prop.

First, we modify the snapshot to deliberately fail the test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//__snapshots__/MyComponent.test.js.snap
exports[`MyComponent should render initial layout 1`] = `
Array [
<div>
    <input
         onChange={[Function]}
         type="text"      
     />
</div>,
]
`;

A consequence of altering the snapshot first is the importance of prop (or attribute) order.

Jest will alphabetically sort props received by the expect function before comparing against the snapshot. Therefore, we must maintain this order.

Executing the test now yields the following error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Received value does not match stored snapshot 1.
Expected:
 - Array [
    <div>
        onChange={[Function]}
        <input type="text”/>
     </div>,
    ]
Actual:
+ Array [
    <div>
        <input type=”text”  />
     </div>,
    ]

To pass this test, we simply provide an empty function to onChange.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// MyComponent.js
import React from 'react';

export default class MyComponent extends React.Component {
  render() {
    return <div><input
      onChange={() => {}}
      type="text" /></div>;
  }
}

Next, we ensure the component’s state updates after the onChange event is dispatched.

This involves creating a new unit test that calls the onChange function in the input, passing an event to simulate a real UI event. Then, we verify that the component state contains a key named input.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// MyComponent.test.js
...
it("should create an entry in component state", () => {
    // given
    const component = shallow(<MyComponent />);
    const form = component.find('input');
    // when
    form.props().onChange({target: {
       name: 'myName',
       value: 'myValue'
    }});
    // then
    expect(component.state('input')).toBeDefined();
});

We are now greeted with this error:

1
Expected value to be defined, instead received undefined

This signifies that the component lacks a state property named input.

We rectify this by introducing this entry into the component’s state.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// MyComponent.js
import React from 'react';

export default class MyComponent extends React.Component {
  render() {
    return <div><input
      onChange={(event) => {this.setState({input: ''})}}
      type="text" /></div>;
  }
}

Our next task is to ensure that a value is assigned to this new state entry, obtained from the event.

Let’s create a test to confirm the state contains this value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// MyComponent.test.js
...
  it("should create an entry in component state with the event value", () => {
    // given
    const component = shallow(<MyComponent />);
    const form = component.find('input');
    // when
    form.props().onChange({target: {
      name: 'myName',
      value: 'myValue'
    }});
    // then
    expect(component.state('input')).toEqual('myValue');
  });
 ~~~

Not surprisingly, we get the following error.

~~
Expected value to equal: "myValue"
Received: ""

Finally, we achieve a passing test by retrieving the value from the event and assigning it as the input value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// MyComponent.js
import React from 'react';

export default class MyComponent extends React.Component {
  render() {
    return <div><input
      onChange={(event) => {
         this.setState({input: event.target.value})}}
      type="text" /></div>;
  }
}

With all tests passing, we can now refactor our code.

Let’s extract the function provided to the onChange prop into a new function called updateState.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// MyComponent.js
import React from 'react';

export default class MyComponent extends React.Component {
  updateState(event) {
    this.setState({
        input: event.target.value
    });
  }
  render() {
    return <div><input
      onChange={this.updateState.bind(this)}
      type="text" /></div>;
  }
}

And there we have it - a simple React.js component constructed using TDD.

Summary

This example demonstrated pure TDD by meticulously following each step, writing minimal code to intentionally fail and subsequently pass tests.

While some steps may appear superfluous, tempting us to bypass them, doing so compromises the purity of our TDD approach.

While less strict TDD processes are valid and can work effectively, my recommendation is to diligently follow each step. Don’t be discouraged if it proves challenging - TDD is a skill that requires dedication to master, but the rewards are well worth the effort.

For those interested in delving deeper into TDD and the related behavior-driven development (BDD), I highly recommend reading “Your Boss Won’t Appreciate TDD” by fellow Toptaler Ryan Wilcox.

Licensed under CC BY-NC-SA 4.0