Testing common redux patterns in react using ava

Testing Common Redux Patterns in React Using AVA

Find out how to define and test common Redux patterns, learn about basic Redux concepts, and write tests for them.

Brought to you by

Semaphore

Introduction

This is the second tutorial in our series on testing React and Redux applications. If you haven't read the first part, we encourage you to do so.

Redux is a library for managing state in React applications. Since the code using Redux is composed of many small pieces, it's very easy to test it. In the previous tutorial, we scaffolded the base for our application and set up AVA. In this tutorial, we will start building our todo application by defining and testing common Redux patterns, learn about basic Redux concepts, and write tests for them.

Prerequisites

For this tutorial, you will need to:

  • have basic knowledge of Redux, and
  • have Redux installed:
npm install --save redux

Actions

Redux doesn't enforce strict conventions, so it doesn't matter what our action looks like as long as it has a type and, optionally, a payload. In larger projects, it's very important to keep actions consistent by using something like redux-actions. However, in this tutorial we're going to keep things simple. Let's create and export our first action creator in src/actions.js:

// src/actions.js
export const TOGGLE_TODO = 'TOGGLE_TODO';

export const toggleTodo = id => ({
  type: TOGGLE_TODO,
  payload: id,
});

When dispatched, this action will cause a todo item of a given id to switch its state, for instance from completed to not completed.

Let's test this action in src/actions.test.js:

// src/actions.test.js
import test from 'ava';
import { toggleTodo, TOGGLE_TODO } from './actions';

test('toggleTodo action', t => {
  t.deepEqual(toggleTodo(5), {
    type: TOGGLE_TODO, 
    payload: 5
  });
});

We can now run npm test to ensure that the test passes.

Reducers

Reducers react to dispatched actions by modifying the store. Let's create a reducer which reacts to the TOGGLE_TODO action:

// src/reducers.js
import { combineReducers } from 'redux';
import { TOGGLE_TODO } from './actions';

export const todos = (state = [], action) => {
  switch (action.type) {
    case TOGGLE_TODO:
      return state.map(t => {
        if (t.id === action.payload) {
          return { ...t, completed: !t.completed };
        }
        return t;
      })
    default:
      return state;
  }
};

export default combineReducers({ todos });

We are exporting individual reducers for testing, and the root reducer for the application.

Let's test this reducer in src/reducers.test.js:

// src/reducers.test.js 
import test from 'ava';
import { todos } from './reducers';
import { toggleTodo } from './actions';

test('todos reducer', t => {
  t.deepEqual(todos([
    { id: 0, completed: false, text: 'buy milk' },
    { id: 1, completed: false, text: 'walk the dog' },
    { id: 2, completed: false, text: 'study' } 
  ], toggleTodo(1)), [
    { id: 0, completed: false, text: 'buy milk' },
    { id: 1, completed: true, text: 'walk the dog' }, 
    { id: 2, completed: false, text: 'study' } 
  ]);
});

Here, we're passing an initial set of todos to the reducer, none of which are completed. As the second argument, we are passing the action which toggles the state of the todo with the given id. We'll run npm test to see if our test passes.

Selectors

Selectors are not as common as actions and reducers, but they are a very convenient way of fetching resources out of the Redux store. They make refactoring easier, we can memorize them with libraries like reselect, and, most importantly, we can test them.

A selector looks as follows:

// src/selectors.js
export const getTodos = state => state.todos;

Selectors can be composed, for example a getFirstTodo selector would use getTodos to retrieve the list of todos.

It's a good idea to test at least the higher-level selectors to ensure that low-level selectors work as well. For example, passing tests for getFirstTodo would guarantee that getTodos works as well.

Let's test getTodos:

// src/selectors.test.js 
import test from 'ava';
import { getTodos } from './selectors'; 

test('getTodos selector', t => {
  const todos = [
    { id: 0, completed: false, text: 'buy milk' },
    { id: 1, completed: false, text: 'walk the dog' },
    { id: 2, completed: false, text: 'study' }
  ];
  t.deepEqual(getTodos({ todos }), todos);
});

Here, we're testing if the selector successfully retrieves todos under the key todos from our store. Run npm test to see if this test passes.

Configure the Store

Finally, let's configure the Redux store using our root reducer. We're also going to add redux-logger as middleware in the development mode to help with debugging. This will log dispatched actions and store changes to the console of our browser's developer tools:

// src/configureStore.js
import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
import createLogger from 'redux-logger';

export default function configureStore(initialState) {
  const middleware = [
    // add common middleware
  ].concat(process.env.NODE_ENV === 'development' ? [
    // add development middleware
    createLogger()
  ] : []);

  const store = createStore(
    rootReducer,
    initialState,
    applyMiddleware(...middleware)
  );

  return store
}

NODE_ENV is an environment variable built into create-react-app, and we can use it to alter behavior of our application as needed. In this case, we are logging Redux only in development.

Now, let's see if we configured our store correctly by dispatching an action in src/index.js:

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
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(
  <App />,
  document.getElementById('root')
);

Start your application by running:

npm start

We should see the following in the console:

redux-log

Dispatching the toggleTodo action successfully completed a todo!

Conclusion

In this tutorial, we learned how to test Redux actions, reducers, and selectors. We now have everything we need to test synchronous Redux code. You can apply this knowledge to testing asynchronous code (a.k.a. "side-effects") as well. For example, dispatching an action could cause an API call, a confirm dialogue, or even dispatching another action.

In the next and final tutorial we will create React components, connect them to the Redux store, and build a simple user interface for our todo application.

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

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.