Testing Redux

Learning Goals

  • Understand the different types of tests related to Redux
  • Know how to test Action Creators and Reducers
  • Understand how to test Containers with mapStateToProps & mapDispatchToProps

Vocab

  • Pure functions A pure function is a function that returns the same output given the same input. It has no side affects and modifies no other variables outside of its scope.
  • Vocab from Starting Up Redux

Getting Started

We’re going to be using the TodoList application that we made when we were exploring Redux for the first time. Go ahead and clone it, and switch to the begin-testing branch:

git clone https://github.com/turingschool-examples/redux-lesson-boilerplate testing-redux
cd testing-redux
git checkout begin-testing
npm i

Breaking Down a Container

A container refers to a component that is connected to the Redux store. Before we determine how/what to test for this container, it is important to understand how the component is connected and how data flows to and from the component.

With a partner, annotate the AddToDoForm.js component and any relevant files that interact with this container. Use your experiences from the Crate project to help you track down what files need to be annotated.

Unit Testing

Unit tests aim to test individual pieces of code as thoroughly as possible. In React apps, these targeted pieces include classes, functions, components, and helper functions. Unit testing makes your life easier as a developer, and makes it simpler to refactor your code. When testing in Redux, most of our tests will be unit tests.

Testing in Redux

In order to test Redux, we need to first consider the pieces that will require testing.

When it comes to redux, what do you think we need to test?

Take a look at the Redux docs and come up with an answer.

What we’ll test:

We’ll need:

  • Action Creator tests
  • Reducer tests
  • Container tests
  • Tests for any Redux Middleware we may be using

For this lesson, we’re just going to focus on the first three.

Testing in Redux can actually be a very pleasant experience, because all the functions you’ll be writing while using Redux are pure. This means that given the same inputs for a function, we’ll always get back the same output. For more explanation of pure functions, check out this post.

Let’s start with Action Creator tests.

Action Creators

Action creators in Redux are functions that return an Action object. Actions describe changes to our Redux store. Action creators are pure, and thus are not too difficult to test.

Stop and Think

Why do we even need Action Creators? Couldn’t we just create objects where we need them?

Action Creators are functions that return a plain object. When testing Action Creators we want to test that the returned object is what we expect, based on the input parameters.

Take for example our addTodo() action.

export const addTodo = (text, id) => {
  return {
    type: 'ADD_TODO',
    text,
    id
  }
};

Given a string as text, (let’s say “Go to Brothers”), with an id of 21, we expect it to return an object with a type of 'ADD_TODO' and the text “Go to Brothers” with an id of 21.

Action Creator Tests

actions/todos.test.js

Step one, let’s just make sure everything is wired up.

import * as actions from '../actions';

describe('actions', () => {
  it('should have a type of ADD_TODO', () => {
    expect(true).toEqual(false);
  });
});

Run npm test to run your suite. We should see it fail, because true doesn’t equal false.

expect(received).toEqual(expected)

Expected value to equal:
  false
Received:
  true

Great, our test suite is ready to run, lets actually write our action test.

import * as actions from '../actions';

describe('actions', () => {
  it('should have a type of ADD_TODO', () => {
    // Setup
    const text = "Go to the Vault";
    const id = 1;
    const expectedAction = {
      type: 'ADD_TODO',
      text: "Go to the Vault",
      id: 1
    };

    // Execution
    const result = actions.addTodo(text, id);

    // Expectation
    expect(result).toEqual(expectedAction);
  });
});

YOUR TURN!

Write two more unit tests to cover the other two Action Creators.

Reducers

Recall that Reducers digest actions that have been dispatched to the store, and then return a new state. In other words, a reducer receives an action, and decides what the new state will be based on the type of the action. We want to test that behavior.

Take a look at our todos reducer.

// reducers/todosReducer.js

const todosReducer = (state=[], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, {id: action.id, text: action.text, completed: false}]
    case 'TOGGLE_TODO':
      return state.map(todo => {
        return todo.id === action.id ? {...todo, completed: !todo.completed} : todo
      })   
    default:
      return state
  }
};

export default todosReducer;

Reducer Tests

Make the new test file:
reducers/todosReducer.test.js

To test this reducer, let’s start by testing what happens if the action is undefined. What we want to have happen in this case is for the reducer to return whatever the state was when the reducer function was called.

import { todosReducer } from '../reducers/todosReducer';

describe('todosReducer', () => {
  it('should return the initial state', () => {
    // Setup
    const expected = [];

    // Execution
    const result = todosReducer(undefined, {});

    // Expectation
    expect(result).toEqual(expected);
  });
});

YOUR TURN!

Test the todos reducer for what we expect to see in State if we…

  • Hit the ADD_TODO case:
it('should return state with a new todo', () => {

});
  • Hit the TOGGLE_TODO case:
it('should toggle the completed status of a new todo', () => {

});

Unit and Integration Testing Containers

Testing React Containers is a lot of what you already know, with a little of what you don’t mixed in. Remember that a container is a Redux connected React component. We connect the React component to the Redux store using the connect method.

Lets take a look at our container to remind ourselves:

// containers/AddTodoFormContainer.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions';

class AddTodoForm extends Component {
  constructor(props) {
    super(props);
    this.state = { text: '' };
  }

  render() {
    const { handleSubmit, todos } = this.props;
    return (
      <section>
        <form onSubmit={ (e) => {
            e.preventDefault();
            handleSubmit(this.state.text, todos.length);
        }}>
          <input  value={this.state.text}
                  placeholder="Add A Todo"
                  onChange={(e) => this.setState({ text: e.target.value} )} />
          <button>Add Todo</button>
        </form>
      </section>
    )
  }
}

const mapStateToProps = (state) => ({
  todos: state.todos
});

const mapDispatchToProps = (dispatch) => ({
  handleSubmit: (text, id) => dispatch(addToDo(text, id))
});


export default connect(mapStateToProps, mapDispatchToProps)(AddTodoForm);

Some Context

In some projects out in the wild, you’ll see the component imported into the container file, rather than all in one file. Both are reasonable approaches, the former being mostly for container reusability.

Let’s Think

Take a moment to think about testing these connected components. Since this component is connected to the Redux store, what might happen when we import component into a test file and call render() (from React Testing Library) with the component? Will it go well for us? Why not?

With a connected component, any time we bring the component into a test, it will try to connect to the Redux store. But if the component is rendered on its own in the test, then it no longer has a Provider wrapped around it (like we would see in the src/index.js file around App).

So it no longer has access to a Redux store and can’t connect! For unit and integration test where we are testing a component that is connected to the store, we must “provide” a redux store for that component to connect to.

Let’s break it down. We need to:

  • Test a component connected to the store
  • Give that component a store to connect to
  • A store is created and given to a Provider component and wrapped around a component
  • So we need to make a store, give it to a provider, and wrap the component under test with that provider

Here is what that could look like in pseudocode:

// AddTodoFormContainer test file

// ..other testing imports up here

// import the function needed to make a store
// import the component that provides the store to whatever it is wrapped around
// import the root reducer

it('can render AddTodoForm and see the form', () => {
  // make a new Redux store

  // render the component with the component under test (AddTodoFormContainer) wrapped in a Provider
  // this part is the crux of it all, so no worries if it's tough

  // check that the form is rendering as expected
});

Try it Out!

Take some time to fill in the pseudocode from above. Take it one line at a time, and phone a friend when you need to!

Testing a Connected Component

// AddTodoForm.test.js

import React from 'react';
import AddTodoForm from './AddTodoForm';

import { render } from '@testing-library/react';
import '@testing-library/jest-dom';

import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { rootReducer } from '../reducers';


describe('AddTodoForm', () => {
  it('can render AddTodoForm and see the form', () => {
    const store = createStore(rootReducer);

    const { getByPlaceholderText } = render(<Provider store={store}><AddTodoForm /></Provider>);

    expect(getByPlaceholderText('Add A Todo')).toBeInTheDocument();
  });
});

If you want to get fancy with it and reduce the boilerplate around each component under test for every it block, then checkout how a special render function can be made from the React Testing Library recipes docs. Keep in mind this is not necessary for your projects.

Testing Functions Being Called

Note that with this style of test, since Redux is giving the component the function to call when the form is submitted, we don’t have direct access to that function to check if it’s called. Therefore, we cannot have the same style test as before where would check if a jest.fn() was invoked with the correct arguments…you can debate in your head for a few minutes on if you like that or not.

Break the Code!

Take out the Provider component in your test so that you’re rendering only the <AddTodoForm />. What happens? What is the error? Why are you getting this error?

Note down this error because you might see it again in the future.

Final Thoughts

Testing Redux can be your favorite thing in the world if you lean into it. All of the pieces of the Redux flow have been designed so that they are easy to test. You can do it, give it a shot!

Resources

Testing Section of Official Redux Docs
Comprehensive Blog Post about Unit Testing Redux
Blog Post About Testing Containers

Lesson Search Results

Showing top 10 results