DEV Community

Cover image for Testing Redux with Jest
Alexei Dulub
Alexei Dulub

Posted on

Testing Redux with Jest

Testing Redux with Jest

Managing the state of an application is certainly a difficult task. Redux, is a predictable state container that has simplified the process for Javascript applications. However simplified the process, testing your code is a necessity.

This article will guide you through the steps and explain what test-driven development is, how you could set up your testing environment, and then guide you through testing actions and reducers that help manage state. It will also take a look at how redux-saga simplifies testing asynchronous actions.

Prerequisites

This post is aimed at developers that have sufficient experience with Javascript and are comfortable with writing applications. A good understanding and experience with Redux will certainly help, as this article focuses mainly on testing state trees managed via Redux. However, if you do not have a lot of experience with Redux, check out the resources section. Some experience with testing will be beneficial.

✅ A sufficient understanding of Redux and state management.

✅ A good understanding of ES6 Javascript generators.

⭐ Make sure to check out the resources section!

Outcomes

By reading this post you will :

  • Have a fundamental understanding of test-driven development.
  • Know how to test Redux state trees and asynchronous actions.

Test-Driven Development: Concept Overview

Test-Driven development is slowly becoming a standard in a lot of new applications, and the reason why this is so is that traditional testing methods are often insufficient or do not give you a complete picture.

Traditional testing often also called naive testing is often performed after a feature or a group of features have been completely developed, this takes up a lot of time and the time taken to fix the issues/bugs shown through these testing rounds is can get long very easily. To address these concerns, software developers follow the test-driven development procedure, mainly there are 3 types of tests that you could write:

  1. Unit Tests - Unit tests test a single function or a component in a large application. This is the kind of test that you will write frequently. The purpose of a unit test is to give you confidence in an isolated piece of code not of the whole application, which is why you need the other 2 types of tests.
  2. Integration Tests - Integration tests a whole bunch of units, something like a component that uses a set of components. Integration tests test the usage of that particular set of units.
  3. Acceptance Tests - Also known as end-to-end tests, are often more complex and consume more resources to run but give higher confidence in your application as a whole. It could be something like simulating the action of an authorized user deleting a post, or adding one.

Regardless of the type of tests, there are mainly 3 steps in any test-driven development lifecycle:

  1. Always write a set of failing tests for a feature.
  2. Write just enough code to pass the failing tests.
  3. Refactor code if possible, to ensure code quality.

Setting up your environment

To get started, you must first install redux and Jest, the testing library we will use. Make sure to have initialized a package.json file in your project directory, then run the following commands to install the dependencies:

npm install redux redux-saga

and then:

npm install --save-dev jest

Once the dependencies have finished installing you must a few lines to your package.json file under the scripts key:

{
  "scripts": {
    "test": "jest",
    "test:watch": "npm test -- --watch"
  },
}
Enter fullscreen mode Exit fullscreen mode

The first script will run all the tests once and show the results then stop, the second script will rerun the tests every time there is a change. You could skip the second script and run the following command instead if you think the second script is redundant:

npm run test -- --watch

Ideally, you would create a directory named tests in your project root, and then all your test files will live inside it. All your test files should follow a common name format of .test.js, meaning that a test file with the name of the abc will have a filename of abc.test.js.

Writing unit tests for actions and reducers

Redux provides the ability to maintain a global state container by following a few steps. When the application's state changes, that part of the application would dispatch what is known as an action (more or less an object) that is directly related to or provides an indication of what happened, a reducer would get hold of this action and perform a change on the state tree that will modify the state, once this is done the global store alerts the part of the application that initiated the change.

To make the actions as consistent as possible, Redux encourages the use of action creators, which are functions that would return the action object with the action type and optionally a payload. Action creators and reducers play a large role in maintaining state, which also makes it necessary for them to be tested.

An action creator with the goal of incrementing some part of the state by a value would look like this:

const incrementBy = (value) => {
    return {
        type: 'INCREMENT_BY',
        payload: value,
    };
};
Enter fullscreen mode Exit fullscreen mode

Where the type indicates the kind of action that is dispatched and the payload contains a value that will be used by the reducer. The goal of testing this action creator, therefore, is to ensure that the created action has a specific type and accepts a payload.

To ensure that the type is more reusable, you could declare it separately and make it exportable:

export const type = 'INCREMENT_BY';

export const incrementBy = (value) => {
    return {
        type,
        payload: value,
    };
};
Enter fullscreen mode Exit fullscreen mode

And then you could test this action creator like this:

it('should create an action with INCREMENT_BY type', () => {
    const value = 5;
    const expectation = {
        type: 'INCREMENT_BY'
        value,
    };

    expect(incrementBy(value)).toEqual(expectation);
});
Enter fullscreen mode Exit fullscreen mode

The it method also known as test in jest is what describes the test case. First, we create an action that we would expect to be created given a specific value, then using the expect and toEqual methods we can see if the action returned by the action creator incrementBy is the same as what we would expect it to be.

A reducer is mostly a bunch of if/else-if statements or a set of switch/case statements that would do a different thing when a different action is passed into it:

const initialState = 0;

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'INCREMENT_BY':
            return state + action.payload;
        case 'DECREMENT_BY':
            return state - action.payload;
        default:
            return state;
    };
};
Enter fullscreen mode Exit fullscreen mode

You would quite often see switch/case instead of if/else-if which is why it has been used here instead, but it is up to you. All the reducer is responsible for is to apply the necessary changes to the current state and then returning the new state, which is the very behavior that is being tested here:

describe('test reducer', () => {

    it('should return 0 as initial state', () => {
        expect(reducer(undefined, {})).toEqual(0);
    });

    it('should handle INCREMENT_BY', () => {
        expect(
            reducer(0, {
                type: 'INCREMENT_BY',
                value: 2,
            })
        ).toEqual(2);

        expect(
            reducer(5, {
                type: 'INCREMENT_BY',
                value: 10,
            })
        ).toEqual(15);
    });

    it('should handle DECREMENT_BY', () => {
        expect(
            reducer(5, {
                type: 'DECREMENT_BY',
                value: 2,
            })
        ).toEqual(3);
    });

});
Enter fullscreen mode Exit fullscreen mode

This test case is somewhat bigger than the previous test case, not because there is something special going on here but because there are 3 different tests. The first test checks if the initial state is returned as expected (which is 0) and then the second test is to check if the state is increased both when it is at 0, the initial state, or some other value. The third test case checks if a particular state value can be decremented.

In this test case in particular, however, we use the describe method, which helps to group up tests that can help us navigate the test output easily.

Testing Redux sagas

So far we have been looking at synchronous actions, once dispatched the state is immediately modified, but there are times when you have to perform an action asynchronously. Testing asynchronous actions are quite difficult which is why middleware like redux-saga, is very handy. Especially when it comes to testability.

Generally there for any action, there must be two stages which will eventually cause changes in the application:

  • The action begins
  • The action finishes successfully or fails

To not deviate from the article's focus, we will create a pseudo-saga and then take a look at how you could test it:

import { call } from 'redux-saga/effects'

const sum = (num1, num2) => num1 + num2

function *sumSaga() {
  let value = 0

  value = yield call(sum, value, 1)  
  value = yield call(sum, value, 2)

  yield value
}
Enter fullscreen mode Exit fullscreen mode

The above saga will sum 2 numbers while pausing each time when it hits the yield keyword, then what is returned by that yield is assigned as the value. Finally, the saga yields the value itself.

it('sumSaga should yield the correct values', () => {
  const gen = sumSaga();
  let value = 0;

  expect(gen.next().value).toEqual(call(sumSaga, value, 1))
  expect(gen.next(1).value).toEqual(call(sumSaga, value, 2))
  expect(gen.next().value).toBeUndefined()

})
Enter fullscreen mode Exit fullscreen mode

You would test a saga almost the same way you would while testing a normal function but sagas have this play/pause kind of behavior that you should take into account while testing. The process of testing a saga is simplified greatly due to the effects exposed via redux-saga, like call() for example.
This test case in particular tests if the sumSaga saga starts and then ends (when the value of the generator is undefined), all the while matching the expectations of what we think our value is.

PixelPlex: Your dream web development company

PixelPlex is a software development company that has been delivering outstanding web development services with over 150 custom projects delivered and 12 years of quality-assured industry experience. Whether it’s an exemplary B2B/B2C full-stack web application or a robust backend capable of handling large amounts of traffic, our team of skilled and experienced developers will come up with an optimal solution that perfectly captures your brand and it’s values.

Add a brand image here if possible

Take a look at our latest work at our website, to get a feel of the variety of products that we have delivered and what our clients say about us! Let us handle all the heavy-lifting, while you sit back and relax knowing that your project is in good hands.

Summary

That was a tough read! Now that you have finished reading the article you must know that:

✅ There are mainly 3 types of tests that you can write when it comes to Test-Driven Development, Unit tests, Integration tests, and Acceptance tests.

✅ Redux is a state management library that helps to make state management predictable and testable. Action creators and reducers in particular are testable.

✅ Redux-Saga is a middleware that promises to make asynchronous actions much easier to test and perform, as compared to its popular alternative Redux-Thunk.

Resources

Feel like reading more? Check these links out:

Top comments (0)