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.
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 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:
- 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'.
- a user clears the filter by clicking the selected position:
expect(
reducer('Dev', {
type: 'SET_POSITION_FILTER',
name: 'Dev'
})
).toEqual(null)
- a user changes the filter's selection, switching between positions:
reducer('Dev', {
type: 'SET_POSITION_FILTER',
name: 'QA'
})
).toEqual('QA')
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".
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.
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'
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.