1 Feb 2017 · Software Engineering

    Testing React Components with AVA

    8 min read
    Contents

    Introduction

    This is the third tutorial in our series on testing a React and Redux application with AVA.

    In this tutorial, we will build the actual UI for our todo application using React. We’ll connect our React components to the Redux store, test them using AVA and Airbnb’s Enzyme, and see how React makes it easy to write both isolated unit tests and full integration tests.

    Building the UI

    Our application currently has a dataflow set up with Redux, but there is no way for the user to interact with it. Let’s create some basic components using React.

    Creating a Todo Item

    We’re going to start off by creating a component for a single todo. This component will output the text and, if completed, have a strikethrough. When we click on it, it will execute a callback with its id. We will use this behavior later for dispatching the toggleTodo action that we created in the previous tutorial. Let’s begin:

    // src/Todo.js
    import React, { PropTypes, Component } from 'react';
    
    class Todo extends Component {
      constructor(props) {
        super(props)
        this._onClick = this._onClick.bind(this);
      }
    
      _onClick() {
        this.props.onToggle(this.props.id);
      }
    
      render() {
        return (
          <li
            style={{
              textDecoration: this.props.completed ? 'line-through' : 'none'
            }}
            onClick={this._onClick}
          >
            {this.props.text}
          </li>
        );
      }
    }
    
    Todo.propTypes = {
      id: PropTypes.number.isRequired,
      text: PropTypes.string.isRequired,
      completed: PropTypes.bool.isRequired,
      onToggle: PropTypes.func.isRequired,
    };
    
    export default Todo;

    Defining expected prop types in components makes it easier for us to catch unexpected types faster, because we’ll know exactly which component is passing the unexpected props.

    In the component’s constructor, we’re binding the _onClick method to the component in order to have access to the props, i.e. to the id of the todo.

    Todo List

    Once we have defined our todo item, we can create another component, which will be a list of todos. This component will be connected to the Redux store because it has to have access to the array of todos. It should also be able to dispatch the toggleTodo action.

    Redux is an independent state management library that can be integrated with any framework, which means that it does not provide React bindings out-of-the-box. So, we need to install react-redux:

    npm install --save react-redux

    It exports a component called Provider. We should use it to wrap our main component App, and pass it our Redux store:

    // src/index.js
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { Provider } from "react-redux";
    import App from './App';
    import './index.css';
    
    import configureStore from './configureStore';
    import { toggleTodo } from './actions';
    
    const store = configureStore({
      todos: [
        { id: 0, completed: false, text: 'buy milk' },
        { id: 1, completed: false, text: 'walk the dog' },
        { id: 2, completed: false, text: 'study' }
      ]
    });
    
    store.dispatch(toggleTodo(1));
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    );

    Using Provider is optional, but recommended if you don’t want to pass the store every time you connect a component to it.

    Another thing that react-redux exports is a function called connect. We will use it to connect our component to the store. Let’s create the component for listing our todos:

    // src/TodoList.js
    import React, { PropTypes } from 'react';
    import { connect } from 'react-redux';
    import Todo from './Todo';
    import { toggleTodo } from './actions';
    
    const TodoList = props => (
      <ul style={{ textAlign: 'left' }}>
        {props.todos.map(todo => (
          <Todo
            key={todo.id}
            {...todo}
            onToggle={props.toggleTodo}
          />
        ))}
      </ul>
    );
    
    TodoList.propTypes = {
      todos: PropTypes.arrayOf(PropTypes.shape({
        id: PropTypes.number.isRequired,
        text: PropTypes.string.isRequired,
        completed: PropTypes.bool.isRequired,
      })).isRequired,
      toggleTodo: PropTypes.func.isRequired
    };
    
    const mapStateToProps = state => {
      return {
        todos: state.todos
      };
    };
    
    const actionCreators = {
      toggleTodo
    };
    
    export default connect(
      mapStateToProps,
      actionCreators
    )(TodoList);

    Since we don’t need any callbacks or component states in TodoList, we were able to use a functional component, which is much shorter to type.

    Add Todos to the App

    Finally, we can include TodoList in our App component:

    // src/App.js
    import React, { Component } from 'react';
    import TodoList from './TodoList';
    import logo from './logo.svg';
    import './App.css';
    
    class App extends Component {
      render() {
        return (
          <div className="App">
            <div className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h2>Welcome to React</h2>
            </div>
            <TodoList />
          </div>
        );
      }
    }
    
    export default App;

    Run npm start to see if the todos have rendered correctly and if clicking on them toggles the strikethrough.

    Testing

    Now that we have built a basic UI, we can start testing. First, we’ll install a couple of dependencies:

    • Enzyme for rendering React components,
    • Test Utilities because Enzyme depends on them,
    • Sinon.JS for testing callbacks, and
    • redux-mock-store for testing dispatched actions.
    npm install --save-dev enzyme react-addons-test-utils sinon redux-mock-store

    Rendering in Enzyme

    We’re using Enzyme because its API is simpler and more powerful than Test Utilities. It exposes three types of rendering React components:

    import { shallow } from 'enzyme'; // shallow rendering
    import { mount } from 'enzyme'; // full DOM rendering
    import { render } from 'enzyme'; // static rendering

    Each type renders a React component in a different way. Shallow rendering is ideal for testing components in isolation, full DOM rendering is better for testing integration, and static rendering is great for making assertions about the rendered HTML.

    Testing the App

    By running npm test we can see that we broke a test:

    3 passed
    1 failed
    
    
    1. App › renders without crashing
    failed with "Could not find "store" in either the context or props of
    "Connect(TodoList)". Either wrap the root component in a <Provider>, or
    explicitly pass "store" as a prop to "Connect(TodoList)"."
    

    This failure is coming from App.test.js. The error message is telling us that we need to wrap App in Provider, so that connect knows which store to connect to:

    // src/App.test.js
    import test from 'ava';
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { Provider } from 'react-redux';
    import configureStore from 'redux-mock-store';
    import App from './App';
    
    const mockStore = configureStore();
    const initialState = { todos: [] };
    
    test('renders without crashing', t => {
      const div = document.createElement('div');
      const store = mockStore(initialState);
      ReactDOM.render(
        <Provider store={store}>
          <App />
        </Provider>,
        div
      );
    });

    We created a store using redux-mock-store. It’s empty because this is a smoke test that checks if our application is rendering successfully.

    Let’s see if we managed to fix the test. After running npm test, you should see the following output:

    4 passed
    

    Testing the Todo Item

    We’ll use shallow rendering for testing the Todo component:

    // src/Todo.test.js
    import React from 'react';
    import test from 'ava';
    import sinon from 'sinon';
    import { shallow } from 'enzyme';
    import Todo from './Todo';
    
    test('outputs given text', t => {
      const wrapper = shallow(
        <Todo
          id={1}
          text="buy milk"
          completed={false}
          onToggle={() => {}}
        />
      );
      t.regex(wrapper.render().text(), /buy milk/);
    });
    
    test('has a strikethrough if completed', t => {
      const wrapper = shallow(
        <Todo
          id={1}
          text="buy milk"
          completed
          onToggle={() => {}}
        />
      );
      t.is(wrapper.prop('style').textDecoration, 'line-through');
    });
    
    test('executed callback when clicked with its id', t => {
      const onToggle = sinon.spy();
      const wrapper = shallow(
        <Todo
          id={1}
          text="buy milk"
          completed={false}
          onToggle={onToggle}
        />
      );
      wrapper.simulate('click');
      t.true(onToggle.calledWith(1));
    });

    The first two times, we’re passing an empty function as onToggle because the component requires it, but the third time we’re actually testing that callback, so we’re creating a spy with Sinon.JS and checking if it had been called with the expected value.

    Testing the Todo List

    The TodoList component is different — it’s connected to the Redux store. By using redux-mock-store we can test if the toggleTodo action is dispatched when we simulate a click on a Todo component:

    // src/TodoList.test.js
    import test from 'ava';
    import React from 'react';
    import { mount } from 'enzyme';
    import { Provider } from 'react-redux';
    import configureStore from 'redux-mock-store';
    import TodoList from './TodoList';
    import { toggleTodo } from "./actions";
    
    const mockStore = configureStore();
    const initialState = {
      todos: [
        { id: 0, completed: false, text: 'buy milk' },
        { id: 1, completed: false, text: 'walk the dog' },
        { id: 2, completed: false, text: 'study' }
      ]
    };
    
    test('dispatches toggleTodo action', t => {
      const store = mockStore(initialState);
      const wrapper = mount(
        <Provider store={store}>
          <TodoList />
        </Provider>
      );
      wrapper.find('Todo').at(0).simulate('click');
      t.deepEqual(store.getActions(), [toggleTodo(0)]);
    });

    Note that TodoList exports a connected component. If we wanted to test it in isolation, we could add a named export to provide access to the pure component:

    // src/TodoList.js
    // ...
    export const TodoList = props =>
    // ...

    This would allow us to access the original component like this:

    import { TodoList } from './TodoList';

    Run the Tests

    To check if our tests pass, let’s run npm test. If everything went well, you should see the following output:

    8 passed
    

    Conclusion

    As you can see, we can cover a lot of functionality using only unit tests, which are really fast. There are many ways to test a React application, so you will need to decide what is the best approach to testing specific components. Sometimes a component is too simple to test. Other times, a component might require multiple layers of testing.

    Keep in mind that these unit tests are just that, unit tests, so they won’t be able to catch specific cross-browser bugs. You will still need to set up end-to-end testing, but writing unit tests will definitely help you catch some bugs earlier.

    If you have any questions or comments, feel free to leave them in the section below.

    P.S. On a related note, if you want to speed up CI for your React project, watch this video by LearnCode.academy about setting up a project on Semaphore.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I'm a front-end designer/developer from Zagreb, Croatia. I got into coding when I was modding Warcraft (I'm not even kidding). One of my greatest passions is bringing content to life on the web.