Skip to content

Instantly share code, notes, and snippets.

@baniol
Last active January 16, 2019 08:35
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save baniol/9ced449620478096d11521442bbd8b54 to your computer and use it in GitHub Desktop.
Save baniol/9ced449620478096d11521442bbd8b54 to your computer and use it in GitHub Desktop.
React/Redux unit testing

React Redux Walkthrough, part 2

Unit testing

I the first part of the series we learned what are the main React/Redux building blocks and how they interact to provide an easy to read and consistent data flow. Before we go further with the development it's a good time to take a step back and see how we can test the code written so far.

The code for the application can be found here - checkout to the 02-unit-testing branch and run npm install.

Note a few changes in the package.json file:

  • mocha - a test runner,
  • expect - npm assertion library.

Tests can be run with npm test command.

Each test file should have the .spec.js extension.

Let's take a closer look at actions, reducers and components and how we can test them.

Actions

As we explained in the first part, action creators are functions that return actions which in turn are objects containing payload of data. In our application we only have one action type SET_POSITION_FILTER called when a user click on the position filter. Therefore, unit testing the action will consist of calling the action creator function and checking the response:

test/actions/index.spec.js

describe('Actions', () => {

  it('setPositionFilter should create SET_POSITION_FILTER action', () => {
    expect(actions.setPositionFilter('Developer')).toEqual({
      type: 'SET_POSITION_FILTER',
      name: 'Developer'
    })
  })

})

Reducers

Reducers are functions that take the previous state and action as parameters and return the next state. They are pure functions meaning they are easy to test. Take employees reducer:

import expect from 'expect'
import reducer from '../../reducers/employees'

describe('employees reducer', () => {
  it('should return the initial state', () => {
    expect(
      reducer(undefined, {}).length
    ).toEqual(10)
  })
})

Our current employee reducer doesn't do much, it just returns the initial state which is an employee collection. So we have to check if for the state undefined we get the collection of a correct length.

For positionFilter reducer we have a little bit more to test, as it takes SET_POSITION_FILTER action string. The possible states of the reducer are null and any string with a position name. We have to check for the following scenarios:

  1. a user clicks on a 'clean' (not activated) filter:
expect(
  reducer(null, {
    type: 'SET_POSITION_FILTER',
    name: 'Dev'
  })
).toEqual('Dev')

This means that the filter's previous state was null (no selection) and the action name is 'Dev' (Dev position clicked). The expected reducer's state should be the action name - 'Dev'.

  1. a user clears the filter by clicking the selected position:
expect(
  reducer('Dev', {
    type: 'SET_POSITION_FILTER',
    name: 'Dev'
  })
).toEqual(null)
  1. a user changes the filter's selection, switching between positions:
reducer('Dev', {
    type: 'SET_POSITION_FILTER',
    name: 'QA'
  })
).toEqual('QA')

Components

Components, compared to actions and reducers, will be a little bit more tricky to unit test. Testing components means checking if they are rendered properly and handle properly props and state.

React comes with a react-addons-test-utils utility that can be used for simulating actions (like clicks), querying elements from components, rendering and mocking components for testing.

For our application testing we'll use the Enzyme library developed by Airbnb team "that makes it easier to assert, manipulate, and traverse your React Components' output".

App component

Let's begin with our simplest component App which just 'embeds' two child components. So testing would mean checking if those components are rendered. The complete code for the component tests (test/components/App.spec.js):

import expect from 'expect'
import React from 'react'
import { shallow } from 'enzyme'
import App from '../../components/App'
import EmployeeList from '../../components/EmployeeList'
import PositionFilter from '../../components/PositionFilter'

let wrapper

describe('App component', () => {
  beforeEach(() => {
    wrapper = shallow(<App/>);
  })
  it('Should render the application', () => {
    expect(wrapper.find(PositionFilter).length).toEqual(1)
    expect(wrapper.find(EmployeeList).length).toEqual(1)
  });
  it('Should contain 2 components', () => {
    expect(wrapper.children().length).toEqual(2)
  })
});

First, we are importing the shallow method from Enzyme. In short, shallow will only render the component we want, contrary to the other Enzyme method - mount - that would render components in an actual environment, such as jsdom.

The line wrapper = shallow(<App/>) gives us a handler that we can use for checking if the App component contains desired elements.

EmployeeList component

This component renders a ul list tag with employees as items. It also takes employees collection as a property. Our task is to check if the elements are rendered accordingly to the passed collection. First, we must shallow render the component and pass a mocked props object.

wrapper = shallow(<EmployeeList {...props} />)

Then we check for rendered li tags:

expect(wrapper.find('li').first().text()).toEqual('Reese Hardin - Web Developer')

we also use a method at for finding a specific index of the collection. We could also use the last method, as the employee collection contains only two item.

Check the API reference for more info on shallow rendering.

An important note to make at this point is that we have to make a small modification to our tested EmployeeList component.

Originally, the module returns a React component wrapped in connect utility that enables passing the state as props to the component. Testing it in this form will not work.

What we really want to test is a 'raw' React component and to make it possible we have to export it: export const EmployeeList = ({employees}) => ( ....

Note the difference in the import syntax in the test file. For importing the 'raw' component we use import { EmployeeList } from '../../components/EmployeeList' (named import), while using import EmployeeList from '../../components/EmployeeList' would mean importing the component wrapped in connect which is exported in the module by default.

The last functionality to test for this component is the employee collection filtering. Again, to enable this we have to explicitly export the getEmployeeList from the EmployeeList component.

NOTE: both 'raw' component and getEmployeeList method are imported with import { EmployeeList, getEmployeeList } from '../../components/EmployeeList'

PositionFilter component

The last and the most challenging component to test is PositionFilter

We begin with shallow rendering and pass some mocked props. Then we check for rendered span elements.

A new element in this component is a function filterPositions passed as a property with the mapDispatchToProps method.

If we want to check if the method is triggered we must set a spy let onFilterClick = expect.createSpy() and set it as a property of the component with enzyme setProps method: wrapper.setProps({filterPositions: onFilterClick}).

Then we look for the element with onClick event and simulate the event. Lastly, we check if it has been called expect(onFilterClick).toHaveBeenCalled()

The last functionality of the component to be tested is highlighting the active filter item.

First, we check if the first element doesn't have the style for the active element (backgroundColor), then we set the currentFilter property as the first element from the position array and check again for the first span in the filter - this time it should be highlighted.

You should now have a basic idea now to test your future components and I strongly encourage you to do so. Regular and consistent unit tests will certainly spare you a lot of time and headache chasing bugs as your application reaches new levels of complexity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment