React continues to be the web framework of choice for many UI developers, second only to jQuery, according to Stack Overflow. It provides an intuitive model for building data-driven user interfaces, and efficiently updates the DOM when this data changes. React pairs nicely with Redux, which enables managing the data that React needs to render interfaces. Redux offers a predictable way to structure and update the data in those frontend applications.

In this tutorial, we will explore how to write 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.

Prerequisites

To follow along with the tutorial, a few things are required:

  1. A basic familiarity with React and Redux
  2. The Jest test runner installed on your machine
  3. The Enzyme testing utility installed on your machine
  4. A CircleCI account
  5. A GitHub account

Getting started

For this project, we will use the Reddit API example app in the Redux docs as our example app. We will use this application to show how you would add tests to a real app, which should help when you are building your own applications.

Before adding the tests, we need to find out what the app does. The best way to do this is to clone the app and run it locally. To clone the application and install its dependencies run:

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. 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 tutorial.

Walkthrough of the application’s functionality

The sample app was designed to display the current headlines in a selected subreddit, by fetching the data from the Reddit API. Users can select the subreddit they want to see headlines for, and they are loaded and displayed on the screen. Users can also update the data displayed for the currently selected subreddit, by clicking a Refresh button.

This simple app is a good example because 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

As we work on our app, we will need to track changes with Git. The first step is to create a new repository, which will help us keep track of our changes separately from the redux repository. Create a new git repo in the async folder by running:

git init

This new repository will track changes only in the async folder, ignoring the rest of the code we pulled when we cloned the redux repository. This is a good time to 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 tutorial is an excellent resource if you are not familiar with GitHub.

By now you should have an understanding of what our example app does and you should have a copy of it on your own GitHub account. Our next step is adding the tests.

Testing React components

Test setup

Now we are going to need to use the testing tools mentioned earlier: Jest and Enzyme.

Continuous integration with Jest and Enzyme graphic

If you check the package.json file, you will notice we already have a test command configured. Start with removing the extra flags given in that command, for example: --env=node --passWithNoTests. We will not need those for the remainder of the tutorial.

 "test": "react-scripts test"

react-scripts comes with jest installed and configured, so we do not need to install it again. We do need to install enzyme though, and its adapter for our version of React:

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

Next, we need to configure enzyme to use the adapter. react-scripts supports configuring testing tools in a src/setupTests.js file.

Create that file and enter:

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

We will use snapshot testing to track changes to components. Using this technique, we take snapshots of our components. When the component’s rendered output changes, we can easily detect the changes made. The snapshots are also readable, so it is an easier way of verifying that components render the expected output.

To use the snapshot technique, 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. Configure this in package.json by adding:

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

Now we are ready to begin the actual testing.

Component tests

We will begin 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.

First, we need to import the App component, but the only export in App.js is the redux-connected version of the component: export default connect(mapStateToProps)(App). We want to test the rendering of the component and not its interaction with redux, so we will need to also export the underlying App component. 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__. That means we need to create the containers folder, then create the __tests__ directory and under it, create an App.test.js file.

App is exported now, so we can now import it by adding to our test file:

import { App } from '../App'

Because we are testing the App component independently of redux, anything that is provided by redux (for example, the component’s props), will need to be provided explicitly. We can add some rendering tests to see how this works in practice. Inside App.test.js, 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. By providing the props we are simulating what redux will do for us in the actual app. jest provides a mock function that we can use in place of the real 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. Notice the jest test runner kick in, 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 find a snapshot version of the component that shows the component’s render output.

Go ahead and 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 shows how we can easily use enzyme to query nested components, (in this case,Picker), and assert that it is rendered with the correct props. I highly recommend digging into enzyme’s docs to see the range of testing utilities it provides.

Next, we will add another test to check for elements that are rendered based on some condition. In this 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, we should add a test that deals with some user interaction. This test can 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 by importing actions, because we will mock some of the functions it provides. In the test, we provide the usual props and then a mockEvent object. We will use the mockEvent object 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. Without it, we would get an error informing us of the missing property: 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 have tested the most challenging parts of the code, and that should give us a good starting point to comfortably add tests for any other component functionality.

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. This app, has both asynchronous and synchronous action creators. Asynchronous action creators are used in conjunction with redux-thunk to enable async operations that do not immediately produce a result, like fetching data. Synchronous action creators return plain objects. 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))
  }
}

Our next task is to create the files that enable testing. Inside src/actions/, create a folder named __tests__ and inside that, create a file called actions.test.js.

We will start 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. We can 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 is all we need to do.

To make our work easier once we proceed to test the async action creators, we can add a test for the receivePosts action creator too. This is how the function looks:

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. Extract this into a new function call that takes the json argument and does the transformation we need. 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()
})

You may notice that in the returned action, there is 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. You can test this on your own by mocking the Date.now function, but for the purposes of this tutorial, we will skip this step.

Now that we have selected the scope of what we need to do, we need to add the test for the receivePosts action creator:

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);
    })
  })
})

Note that we are using toMatchObject to match just a subset of the returned action object, which excludes matching the receivedAt key.

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

Time 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 will 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 also need to install a few new packages:

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

We will use fetch-mock to mock HTTP requests made using fetch and redux-mock-store to help us create a mock store to use in the tests. 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 will 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. That is so our mock implementation is not used in other tests.

Next we mock 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. We will also exclude the receivedAt property in the RECEIVE_POSTS action, as in the previous test, and 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. Next we will review how to test reducers.

Testing reducers The reducers are at the heart of redux because 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 are 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 of which manages its own part of the state. Eventually they will be merged into a single root reducer using combineReducers. We are going to export the individual reducer functions to make testing more convenient by adding this snippet to reducers/index.js

export { postsBySubreddit, selectedSubreddit }

Create a __tests__ directory under reducers then create a reducers.test.js file inside that directory, which is where our tests will go. Because it is the simpler of the two, we will test the selectedSubreddit reducer first.

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.

Now we can move on 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
      }
    })
  })
})

Start by testing that it initializes the state correctly. In this case, the default state is an empty object, as shown in the first test.

The tests for the rest of the actions are similar; 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 (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 that section of the tutorial complete, we have covered many parts of a React and redux application that you would need to test in a typical app.

Continuous integration with GitHub and CircleCI

This is the part of the tutorial where we add continuous integration with CircleCI. Continuous integration helps us ensure that any changes we make to the code do not 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 is 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

I would like to take a moment to explore some of the concepts covered here:

  • The docker image specifies the base docker container we are 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 are 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. The /home/circleci/.npm folder 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. If this file changes, a new cache will be created.
  • It is also worth noting that caches are immutable in CircleCI . That means that once a cache is created, there is 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. 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 that runs our tests.

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

Integrating CircleCI and GitHub

Take a moment to make sure you have pushed all your changes to the GitHub repository we created earlier. We will now set up CircleCI for to test our code whenever we make any new changes.

Here is how to add the project to CircleCI:

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

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

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

CircleCI will now run our test steps. In a short while, you should get a successful build. 🎉

With our CI process set up, any new commit that is pushed in the repository will trigger a test run to make sure none of our changes will break the build. In cases where changes we made cause the tests to fail, we will be notified, and we can track exactly which commit caused the failure.

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.