React has taken the frontend world by storm, and for a good reason. It provides an intuitive model for building data-driven user interfaces. React enables us to write declarative components describing the user interface and provide the data needed to render these interfaces. It then efficiently updates the DOM when this data changes. Redux, on the other hand, enables managing the data that React needs to render interfaces. It offers a predictable way to structure and update the data in frontend applications and pairs quite nicely with React.

Now that we have an idea of how React and Redux work together, we will explore how to go about writing tests for an existing React and Redux application. We will then configure continuous integration with CircleCI, to automate the testing and ensure that any new code we add does not break the existing functionality.

To get the most out of this article, we assume a basic familiarity with React and Redux. If you’re new to React, the Intro to React tutorial provides a good overview of how React works. The Learn Redux section in the Redux docs provides lots of learning resources and the Redux examples page is also a good resource for exploring sample apps and getting a feel for how Redux works.

With this info out of the way, let’s go ahead and get started.

Getting started

In this article, we are going to use the Reddit API example app in the Redux docs as our example app. We’ll use this application to demonstrate how to go about adding tests to a real app, similar to what you would encounter while building your own apps.

Before adding the tests, we need to explore what the app currently does. The best way to do this is to clone the app and run it locally. Let’s run the following commands to clone the application, install its dependencies, and finally run it:

git clone https://github.com/reactjs/redux.git
cd redux/examples/async
git checkout db5f8d1
npm install
npm start

Note: The git checkout command checks out the redux repo to the latest commit at the time of this writing i.e. db5f8d1. By the time you read this, there might be some more changes made to the repo, so running this command ensures that we all have the same starting point for the purposes of following this article.

Once the app is running, you should see a screen similar to this one:

Walkthrough of the application’s functionality

The main functionality of the app is to display the current headlines in a selected subreddit, by fetching this data from the Reddit API. It enables the user to select the subreddit they would like to see headlines for, which are then loaded and displayed on the screen. It also enables users to update the data displayed for the currently selected subreddit, by clicking the Refresh button.

While this may seem like a simple app, it has all the components we need for most real-world apps, including:

  • Fetching data from an API
  • User interactions
  • Synchronous and asynchronous actions
  • Presentational and container components

Keeping track of our progress with Git and GitHub

Since we will be making changes to the app, we will need to track these changes with Git. We will create a new repository, which will help us keep track of our changes separately from the redux repository. Let’s create a new git repo in the async folder by running:

git init

This new repository will only track changes in the async folder, ignoring the rest of the code we pulled when we cloned the redux repository. At this point, let’s make an initial commit in our new git repository marking the point where we imported the code from redux:

git add .
git commit -m "Import async example from redux"

We will also need to create a new repository on GitHub where we will push our code. This will come in handy when we will need to integrate with CircleCI later on. Go ahead and create a new GitHub repo and push the code we have to the repo.

This guide is an excellent resource you can refer to if you’re not yet familiar with GitHub.

This concludes our exploration of the Reddit API app. By now you should have an understanding of what the app does and you should have a copy of the app on your own GitHub account. We will now proceed to add the tests.

Testing React components

Test setup

We are going to need some tools to get started with testing:

  • Jest - Test runner
  • Enzyme - Testing utility for React

If you check the package.json file, you will notice we already have a test command configured. Let’s start with removing the extra flags given in that command, i.e. --env=node --passWithNoTests, as we are not going to need those going forward.

After removing the flags, the test command should look like this:

"test": "react-scripts test"

react-scripts comes with jest installed and configured, hence we won’t need to install it again. However, let’s install enzyme now, along with its adapter, for our version of React:

npm install --save-dev enzyme enzyme-adapter-react-16

We are also going to need to configure enzyme to use the adapter. react-scripts supports configuring testing tools in a src/setupTests.js file.

So go ahead and create that file and add the following contents:

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
    
Enzyme.configure({ adapter: new Adapter() });

In our tests, we are going to make use of snapshot testing to track changes to components. This technique enables taking snapshots of our components, and when the component’s rendered output changes, it helps us to easily detect the changes made. The snapshots are also readable, so it’s an easier way of verifying that components render the expected output.

To enable this, we need to install the enzyme-to-json package to convert our React components to a snapshot during testing:

npm install --save-dev enzyme-to-json

We also need to configure jest to use this package as the snapshot serializer. We’ll configure this in package.json by adding:

"jest": {
    "snapshotSerializers": [
      "enzyme-to-json/serializer"
    ]
  }

Now we are set up to begin the actual testing.

Component tests

Let’s start off by writing tests for the App component. A good starting point is adding a snapshot test to ensure that the component renders the expected output, given the required props.

To start off, we need to import the App component. However, the only export in App.js is the redux-connected version of the component, i.e. export default connect(mapStateToProps)(App). We are interested in testing the rendering of the component and not its interaction with redux, therefore, we will need to also export the underlying App component and we will do this by adding this snippet to App.js:

export { App };

To recap, this is how the App.js file should look right now:

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { selectSubreddit, fetchPostsIfNeeded, invalidateSubreddit } from '../actions'
import Picker from '../components/Picker'
import Posts from '../components/Posts'
    
class App extends Component {
  static propTypes = {
    selectedSubreddit: PropTypes.string.isRequired,
    posts: PropTypes.array.isRequired,
    isFetching: PropTypes.bool.isRequired,
    lastUpdated: PropTypes.number,
    dispatch: PropTypes.func.isRequired
  }
  componentDidMount() {
    const { dispatch, selectedSubreddit } = this.props
    dispatch(fetchPostsIfNeeded(selectedSubreddit))
  }
  componentWillReceiveProps(nextProps) {
    if (nextProps.selectedSubreddit !== this.props.selectedSubreddit) {
      const { dispatch, selectedSubreddit } = nextProps
      dispatch(fetchPostsIfNeeded(selectedSubreddit))
    }
  }
  handleChange = nextSubreddit => {
    this.props.dispatch(selectSubreddit(nextSubreddit))
  }
  handleRefreshClick = e => {
    e.preventDefault()
    const { dispatch, selectedSubreddit } = this.props
    dispatch(invalidateSubreddit(selectedSubreddit))
    dispatch(fetchPostsIfNeeded(selectedSubreddit))
  }
  render() {
    const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props
    const isEmpty = posts.length === 0
    return (
      <div>
        <Picker value={selectedSubreddit}
                onChange={this.handleChange}
                options={[ 'reactjs', 'frontend' ]} />
        <p>
          {lastUpdated &&
            <span>
              Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
              {' '}
            </span>
          }
          {!isFetching &&
            <button onClick={this.handleRefreshClick}>
              Refresh
            </button>
          }
        </p>
        {isEmpty
          ? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
          : <div style={{ opacity: isFetching ? 0.5 : 1 }}>
              <Posts posts={posts} />
            </div>
        }
      </div>
    )
  }
}
    
const mapStateToProps = state => {
  const { selectedSubreddit, postsBySubreddit } = state
  const {
    isFetching,
    lastUpdated,
    items: posts
  } = postsBySubreddit[selectedSubreddit] || {
    isFetching: true,
    items: []
  }
  return {
    selectedSubreddit,
    posts,
    isFetching,
    lastUpdated
  }
}
    
export default connect(mapStateToProps)(App)
    
export { App };

By convention, jest will find test files with names ending in .test.js under any folder named __tests__. Therefore, under the containers folder, let’s create the __tests__ directory and under it, we’ll create an App.test.js file.

Since App is exported now, we can now import it as follows in our test file:

import { App } from '../App'

Since we are testing the App component independently of redux, anything that is currently provided by redux, i.e. the component’s props, will have to be provided explicitly. Let’s add some rendering tests to see how this works in practice. Inside App.test.js, let’s add our first test:

import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { App } from '../App'
    
describe('App', () => {
  it('renders without crashing given the required props', () => {
    const props = {
      isFetching: false,
      dispatch: jest.fn(),
      selectedSubreddit: 'reactjs',
      posts: []
    }
    const wrapper = shallow(<App {...props} />)
    expect(toJson(wrapper)).toMatchSnapshot()
  })
})

In this test, we want to verify that the App renders given all the required props. We provide the props thus simulating what redux will do for us in the actual app. jest provides a mock function which we can use in place of an actual function in our tests. For this case, we use this to mock the dispatch function. This function will be called in place of the actual dispatch function in our tests.

You can run the tests with the npm test command. You will notice the jest test runner kick in and run the tests and print out a test run summary. You should also see this message:

1 snapshot written from 1 test suite.

If you open up src/containers/__tests__/__snapshots__/App.test.js.snap you should see a snapshot version of the component that shows the component’s render output.

Let’s add a couple more tests to test the rendering behavior of App. First, we will add a test to ensure that the selectedSubreddit prop is always passed to the Picker component. We will add this test just below our existing tests:

// Add this import
import Picker from '../../components/Picker'
    
it('sets the selectedSubreddit prop as the `value` prop on the Picker component', () => {
    const props = {
      isFetching: false,
      dispatch: jest.fn(),
      selectedSubreddit: 'reactjs',
      posts: []
    }
    const wrapper = shallow(<App {...props} />)
    // Query for the Picker component in the rendered output
    const PickerComponent = wrapper.find(Picker)
    expect(PickerComponent.props().value).toBe(props.selectedSubreddit)
  })

This test demonstrates how we can easily use enzyme to query nested components, i.e. Picker, and assert that it is rendered with the correct props. I highly recommend digging into enzyme’s docs to see the various testing utilities it provides.

Next, we will add another test to check for elements that are rendered based on some condition. For our case, we will verify that the Refresh button is rendered when the isFetching prop is false:

it('renders the Refresh button when the isFetching prop is false', () => {
    const props = {
      isFetching: false,
      dispatch: jest.fn(),
      selectedSubreddit: 'reactjs',
      posts: []
    }
     const wrapper = shallow(<App {...props} />)
     expect(wrapper.find('button').length).toBe(1)
  })

Finally, let’s add a test that deals with some user interaction. Let’s verify that when the Refresh button is clicked, it dispatches the correct actions:

// Add this import
import * as actions from '../../actions';
    
it('handleRefreshClick dispatches the correct actions', () => {
    const props = {
      isFetching: false,
      dispatch: jest.fn(),
      selectedSubreddit: 'reactjs',
      posts: []
    }
    // Mock event to be passed to the handleRefreshClick function
    const mockEvent = {
      preventDefault: jest.fn()
    }
    // Mock the actions we expect to be called
    actions.invalidateSubreddit = jest.fn();
    actions.fetchPostsIfNeeded = jest.fn();
    
    const wrapper = shallow(<App {...props} />)
    // Call the function on the component instance, passing the mock event
    wrapper.instance().handleRefreshClick(mockEvent);
      
    expect(mockEvent.preventDefault).toHaveBeenCalled();
    expect(props.dispatch.mock.calls.length).toBe(3);
    expect(actions.invalidateSubreddit.mock.calls.length).toBe(1);
    expect(actions.fetchPostsIfNeeded.mock.calls.length).toBe(2);
  })

We need to start off with importing actions. This is needed because we will mock some of the functions it provides. In the test, we provide the usual props and then a mockEvent object, which we will use to simulate a click event sent by the browser when the button is clicked. The mocked event needs to contain a preventDefault property, which should be a function as it will be called inside the handleRefreshClick function. If this is not provided, we would get an error informing us of the missing property, i.e. e.preventDefault is not a function.

Once we render the component using shallow, we then manually call handleRefreshClick, passing in the mock event to simulate what will happen when the function is called in our app. We assert the following properties of our app:

  • event.preventDefault should have been called once.
  • props.dispatch should have been called 3 times.
  • actions.invalidateSubreddit should have been called once.
  • actions.fetchPostsIfNeeded should have been called twice.
    • The first call happens in componentDidMount
    • The second call happens inside handleRefreshClick

To make sure our expectations of the componentDidMount function calls are correct, we can include these assertions right before the handleRefreshClick function call.

const wrapper = shallow(<App {...props} />)
// The next assertions are for functions called in componentDidMount
expect(props.dispatch.mock.calls.length).toBe(1);
expect(actions.fetchPostsIfNeeded.mock.calls.length).toBe(1);
    
wrapper.instance().handleRefreshClick(mockEvent);
    
//... rest of test omitted for brevity

At this point, we’ve tested the most challenging parts of the code and these tests should provide a good starting point to comfortably add tests for any other component functionality.

Some potential tests we could add include:

  • The different rendering paths, e.g. when isFetching is true or when lastUpdated is provided
  • What should happen when the onChange function is called
  • What happens when App receives a new selectedSubreddit prop

What are the relevant tests for the Posts and Picker components? I encourage you to explore this some more and try to write out some of those tests.

Testing Redux functionality

In this section, we will add some tests for the redux related parts of our application, specifically the actions and the reducers.

Testing action creators

We will start off with the action creators. In this app, we have synchronous action creators, which return plain objects, and asynchronous action creators, which are used in conjunction with redux-thunk to enable async operations that don’t immediately produce a result, like fetching data. We will cover how to test both.

For reference, this is how our src/actions/index.js file looks:

export const REQUEST_POSTS = 'REQUEST_POSTS'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'

export const selectSubreddit = subreddit => ({
  type: SELECT_SUBREDDIT,
  subreddit
})

export const invalidateSubreddit = subreddit => ({
  type: INVALIDATE_SUBREDDIT,
  subreddit
})

export const requestPosts = subreddit => ({
  type: REQUEST_POSTS,
  subreddit
})

export const transformResponseBody = (json) => {
  return json.data.children.map(child => child.data);
}

export const receivePosts = (subreddit, json) => ({
  type: RECEIVE_POSTS,
  subreddit,
  posts: transformResponseBody(json),
  receivedAt: Date.now()
})

const fetchPosts = subreddit => dispatch => {
  dispatch(requestPosts(subreddit))
  return fetch(`https://www.reddit.com/r/${subreddit}.json`)
    .then(response => response.json())
    .then(json => dispatch(receivePosts(subreddit, json)))
}

const shouldFetchPosts = (state, subreddit) => {
  const posts = state.postsBySubreddit[subreddit]
  if (!posts) {
    return true
  }
  if (posts.isFetching) {
    return false
  }
  return posts.didInvalidate
}

export const fetchPostsIfNeeded = subreddit => (dispatch, getState) => {
  if (shouldFetchPosts(getState(), subreddit)) {
    return dispatch(fetchPosts(subreddit))
  }
}

Let’s start by creating the necessary files to enable testing. Inside src/actions/ let’s create a folder named __tests__ and inside that, let’s create a file called actions.test.js.

We will get started with the synchronous action creators, which are simply pure functions which take some data and return an action object. We should check that given the necessary arguments, the action creator returns the correct action. Let’s demonstrate this with a test for the selectSubreddit action creator which accepts a subreddit as an argument then returns an action.

import * as actions from '../index'

describe('actions', () => {
  const subreddit = 'reactjs'

  describe('selectSubreddit', () => {
    it('should create an action with a given subreddit', () => {
      const expectedAction = {
        type: actions.SELECT_SUBREDDIT,
        subreddit
      }
      expect(actions.selectSubreddit(subreddit)).toEqual(expectedAction)
    })
  })
})

For most synchronous action creators, that’s all we need to do.

Let’s also add a test for the receivePosts action creator too, since it will make our work easier once we proceed to test the async action creators. This is how the function looks currently:

export const receivePosts = (subreddit, json) => ({
  type: RECEIVE_POSTS,
  subreddit,
  posts: json.data.children.map(child => child.data),
  receivedAt: Date.now()
})

In the returned action, we have a transformation happening in the posts property. Let’s extract this into a new function call that takes the json argument and does the transformation we need. So our new version of the receivePosts function will be as follows. Also, take note that we have to export the new helper function so that we can access it in the tests later on.

export const transformResponseBody = (json) => {
  return json.data.children.map(child => child.data);
}

export const receivePosts = (subreddit, json) => ({
  type: RECEIVE_POSTS,
  subreddit,
  posts: transformResponseBody(json),
  receivedAt: Date.now()
})

In addition, you’ll notice that in the returned action, there’s a receivedAt property, which returns Date.now(). In our test, we will skip testing this property since it changes each time the function is called. There are ways to test this, for example, mocking the Date.now function and making it return a specific return value, but for the purposes of this article, we’ll just skip matching that property.

Now that we’ve selected the scope of what we need to do, let’s add the test for the receivePosts action creator now:

describe('actions', () => {
  const subreddit = 'reactjs'
  // Add the mockJSON response
  const mockJSON = {
    data: {
      children: [{ data: { title: "Post 1" } }, { data: { title: "Post 2" } }]
    }
  };
  
  // ... other tests...
  
  describe('receivePosts', () => {
    it('should create the expected action', () => {
      const expectedAction = {
        type: actions.RECEIVE_POSTS,
        subreddit,
        posts: actions.transformResponseBody(mockJSON),
      }
      expect(actions.receivePosts(subreddit, mockJSON)).toMatchObject(expectedAction);
    })
  })
})

Take note that we’re using toMatchObject to match just a subset of the returned action object, which excludes matching the receivedAt key.

The tests for the rest of the synchronous action creators will follow the same process where given some data, we test that the correct action is returned.

Let’s proceed to test async action creators, and specifically, the fetchPosts action creator. The first thing we need to do is to export the function, and we’ll do this by adding export to the function so it becomes:

export const fetchPosts = subreddit => dispatch => {
  dispatch(requestPosts(subreddit))
  return fetch(`https://www.reddit.com/r/${subreddit}.json`)
    .then(response => response.json())
    .then(json => dispatch(receivePosts(subreddit, json)))
}

We’ll also need to install a few new packages:

npm install --save-dev fetch-mock redux-mock-store

We’ll use fetch-mock to mock HTTP requests made using fetch and we’ll use redux-mock-store to help us create a mock store to use in the tests. Let’s add the tests as follows:

// Add the new imports
import thunk from 'redux-thunk'
import fetchMock from 'fetch-mock'
import configureMockStore from 'redux-mock-store'

const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)

describe('actions', () => {
  const subreddit = 'reactjs'
  const mockJSON = {
    data: {
      children: [{ data: { title: "Post 1" } }, { data: { title: "Post 2" } }]
    }
  };
  
  // ... other tests...
  
  describe("fetchPosts", () => {
    afterEach(() => {
      // restore fetch() to its native implementation
      fetchMock.restore()
    })

    it("creates REQUEST_POSTS and RECEIVE_POSTS when fetching posts", () => {
      // Mock the returned data when we call the Reddit API
      fetchMock.getOnce(`https://www.reddit.com/r/${subreddit}.json`, {
        body: mockJSON
      })

      // The sequence of actions we expect to be dispatched
      const expectedActions = [
        { type: actions.REQUEST_POSTS },
        {
          type: actions.RECEIVE_POSTS,
          subreddit,
          posts: actions.transformResponseBody(mockJSON)
        }
      ]

      // Create a store with the provided object as the initial state
      const store = mockStore({})

      return store.dispatch(actions.fetchPostsIfNeeded(subreddit)).then(() => {
        expect(store.getActions()).toMatchObject(expectedActions)
      })
    })
  })
})

We start with all the necessary imports, including redux-thunk. For this case, we need to configure an actual store, and this means that we’ll also apply the middleware to the mock store as well.

Moving on, we have an afterEach function which runs after each test and ensures that we restore the original fetch implementation so that our mock implementation isn’t used in other tests. In the test, we start by mocking the request we expect to be made and provide a mock body that will be returned as the response body.

We then define the sequence of actions we expect to be taken when we call fetchPosts. This sequence implies that when fetchPosts is dispatched, it should generate a REQUEST_POSTS action, then RECEIVE_POSTS with the posts for the requested subreddit. For this, we’ll also exclude the receivedAt property in the RECEIVE_POSTS action, as in the previous test, and also add the transformed response body in the posts key, as we did earlier.

Next, we create the store, giving it some initial state then dispatch the fetchPosts. Finally, we assert that the list of actions applied to the store should match the sequence in our expectedActions array.

Re-running our tests at this point should confirm that everything passes. This concludes our action creators testing and we’re going to look at how to test reducers next.

Testing reducers The reducers are at the heart of redux since they are how we update the state of our whole application. The reducer tests should help us verify that each of our dispatched actions updates the state as expected.

Here are the contents of the reducers/index.js file that we’re going to test:

import { combineReducers } from 'redux'
import {
  SELECT_SUBREDDIT, INVALIDATE_SUBREDDIT,
  REQUEST_POSTS, RECEIVE_POSTS
} from '../actions'

const selectedSubreddit = (state = 'reactjs', action) => {
  switch (action.type) {
    case SELECT_SUBREDDIT:
      return action.subreddit
    default:
      return state
  }
}
const posts = (state = {
  isFetching: false,
  didInvalidate: false,
  items: []
}, action) => {
  switch (action.type) {
    case INVALIDATE_SUBREDDIT:
      return {
        ...state,
        didInvalidate: true
      }
    case REQUEST_POSTS:
      return {
        ...state,
        isFetching: true,
        didInvalidate: false
      }
    case RECEIVE_POSTS:
      return {
        ...state,
        isFetching: false,
        didInvalidate: false,
        items: action.posts,
        lastUpdated: action.receivedAt
      }
    default:
      return state
  }
}
const postsBySubreddit = (state = { }, action) => {
  switch (action.type) {
    case INVALIDATE_SUBREDDIT:
    case RECEIVE_POSTS:
    case REQUEST_POSTS:
      return {
        ...state,
        [action.subreddit]: posts(state[action.subreddit], action)
      }
    default:
      return state
  }
}
const rootReducer = combineReducers({
  postsBySubreddit,
  selectedSubreddit
})
export default rootReducer

In our reducer file, we have two reducers, each managing its own part of the state, which will eventually be merged into a single root reducer using combineReducers. We’re going to export the individual reducer functions to make testing more convenient, by adding this snippet to reducers/index.js

export { postsBySubreddit, selectedSubreddit }

Let’s go ahead and create a __tests__ directory under reducers then create a reducers.test.js file inside that directory, which is where our tests will go. Let’s test the selectedSubreddit reducer first as it’s the simpler of the two.

import {
  SELECT_SUBREDDIT, INVALIDATE_SUBREDDIT,
  REQUEST_POSTS, RECEIVE_POSTS
} from '../../actions'
import { postsBySubreddit, selectedSubreddit } from '../index'

describe('app reducer', () => {
  describe('selectedSubreddit', () => {
    it('should return the default state', () => {
      expect(selectedSubreddit(undefined, {})).toBe('reactjs')
    })
    
    it('should update the selectedSubreddit', () => {
      const subreddit = 'frontend'
      const action = {
        type: SELECT_SUBREDDIT,
        subreddit
      }
      expect(selectedSubreddit(undefined, action)).toBe(subreddit)
    })
  })
})

Our first test checks that the selectedSubreddit reducer correctly initialises the state. When given an undefined state, or an empty action, it should return the default value, which is set to reactjs. The next check verifies that when the reducer receives a valid action object, it correctly updates the state.

Let’s proceed to the postsBySubreddit reducer.

describe('postsBySubreddit', () => {
  const subreddit = 'frontend'

  it('should return the default state', () => {
    expect(postsBySubreddit(undefined, {})).toEqual({})
  })

  it('should handle INVALIDATE_SUBREDDIT', () => {
    const action = {
      type: INVALIDATE_SUBREDDIT,
      subreddit
    }
    expect(postsBySubreddit({}, action)).toEqual({
      [subreddit]: {
        isFetching: false,
        didInvalidate: true,
        items: []
      }
    })
  })

  it('should handle REQUEST_POSTS', () => {
    const action = {
      type: REQUEST_POSTS,
      subreddit
    }
    expect(postsBySubreddit({}, action)).toEqual({
      [subreddit]: {
        isFetching: true,
        didInvalidate: false,
        items: []
      }
    })
  })

  it('should handle RECEIVE_POSTS', () => {
    const posts = ['post 1', 'post 2']
    const receivedAt = Date.now()
    const action = {
      type: RECEIVE_POSTS,
      subreddit,
      posts,
      receivedAt
    }
    expect(postsBySubreddit({}, action)).toEqual({
      [subreddit]: {
        isFetching: false,
        didInvalidate: false,
        items: posts,
        lastUpdated: receivedAt
      }
    })
  })
})

We should start by testing that it initializes the state correctly, and in this case, the default state is an empty object, as you can see in the first test.

The tests for the rest of the actions are quite similar, where we verify that given an action, the reducer returns the expected state update. The subreddit should be set as the key of the returned object and the nested object should be updated according to the rules we have in the reducer.

You will notice that the common theme with reducers is that given a particular set of inputs, that is, the initial state and an action, a new state should be returned. We do all of our assertions on the returned state to ensure it is what we expect.

With these tests, we have covered the various parts of a React and redux application that you would need to test in a typical app.

Continuous integration with GitHub and CircleCI

At this point, we’ve covered adding tests for the various parts of a React and Redux application. Let’s now see how to add continuous integration with CircleCI. Continuous integration helps us ensure that any changes we make to the code don’t break any existing functionality. Our tests will be run any time we push new code, whether by adding new commits to an existing branch or by opening a pull request to merge a new branch into the main branch. This helps in catching bugs early in the development process.

CircleCI configuration

The first thing we need to add is a configuration file that will tell CircleCI how to test our application. The config file needs to be in a .circleci directory in our root folder and should be named config.yml.

For our application, here’s the config file we’ll use:

version: 2
jobs:
  build:
    working_directory: ~/redux-async
    docker:
      - image: circleci/node:8
    steps:
      - checkout
      - restore_cache:
          key: npm-cache-v1-{{ checksum "package-lock.json" }}
      - run:
          name: Install Dependencies
          command: npm ci
      - save_cache:
          key: npm-cache-v1-{{ checksum "package-lock.json" }}
          paths:
            - /home/circleci/.npm
      - run:
          name: Run Tests
          command: npm test

Let’s go over some of the concepts covered here:

  • The docker image specifies the base docker container we’re going to use. In our case, it will be a container pre-installed with Node.js version 8. All the commands that follow will be run in an instance of this container image.
  • The checkout step checks out the source code to the working directory.
  • CircleCI supports caching dependencies so we’re taking advantage of this to cache our npm dependencies.
  • The restore_cache step restores any cache that is available from a previous build.
  • In the run step, we use npm ci to install our project dependencies.
  • The save_cache step is where we save our cache of the npm dependencies, specifically the /home/circleci/.npm folder, which is where the npm cache is stored when we use npm ci to install the dependencies.
  • We create a cache that uses a checksum of the contents of the package-lock.json file, so if this file changes, a new cache will be created.
  • It’s also worth noting that caches are immutable in CircleCI, meaning that once a cache is created, there’s no changing it afterwards. To change a cache, you need to create a new one entirely. The v1 part of our cache key helps us to invalidate the cache. So in this case, if we needed to manually force the cache to be re-created, we could change that to v2.
  • The final command is the actual test command which runs our tests.

To get a much better overview of the CircleCI config format and all the options available for a JavaScript project, you can refer to https://circleci.com/docs/2.0/language-javascript/.

Integrating CircleCI and GitHub

At this point, let’s make sure we’ve pushed all our changes to the GitHub repository we created earlier. We are now going to integrate CircleCI for continuous testing of our code when we make any new changes.

Here’s how to add the project to CircleCI:

  • Create a new account on CircleCI if you have not already created one.
  • Once you are logged in, ensure your account is selected on the top left corner.

  • Click Add Projects.
  • On the next screen, search for the name of your GitHub repository then click Set Up Project next to it.

  • On the next page, scroll down to Next Steps and click Start Building.

  • CircleCI will now run our test steps and in a short while, you should see a successful build. 🎉

With our CI process set up, any new commit that is pushed in the repository will trigger a test run to ensure we don’t introduce changes that break the build. In cases where we might introduce changes that make the tests fail, we will be notified and can track exactly which commit causes the tests to fail.

Conclusion

This concludes our exploration of adding tests to a real-world React and Redux application. It is my hope that the concepts covered here will be helpful and will set you up for success when working on similar applications in the future.

The following resources were very helpful and will definitely help out in providing more information on testing React and redux:


Kevin Ndung’u is a software developer and open source enthusiast currently working as a software engineer at Andela. He is passionate about sharing his knowledge through blog posts and open source code. When not building web applications, you can find him watching a game of soccer.