Testing react components with ava

Testing React Components with AVA

Learn how to test React components, both in isolation and when connected to the Redux store.

Brought to you by

Semaphore

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:

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.

6d95827ad2de5f374947b02983d4c85b
Matija Marohnić

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.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.