While designing and working on a bunch of React applications I always wanted myself and my team to write unit tests. I was able to learn, write and improve on how we have been writing test cases as a team.
Well, there isn’t any standard way or a rule book on how to and what to unit test with React and Redux, therefore this blog is going to talk about a few patterns that I have started using to unit test my applications. My examples in this document will cover the breadth of how to write unit tests.
What we’ll cover by the end of this series
- Important libraries that we’d use to Unit Test
- Testing Do’s and Don’t’s in a react component
- Identifying the “Component Contract” to test with an example
- Testing redux asynchronous action creators
- Testing redux reducers
- Configuring Jest code coverage report
Important libraries with React for Unit Tests
Jest
Jest is the official Facebook written library for testing react apps, though there are other libraries that help us unit test react, this stands for the following reasons.
- Easy setup, comes built-in with react-create-app
- Code Coverage in the box – no additional libraries required
- Assertion module uses jasmine, making it familiar for most javascript developers
There are many more to it, and you may read about it here.
Enzyme
The enzyme is a jQuery type framework on top of the react written by Airbnb. The enzyme helps you traverse, assert, and manipulate react components. The most important is that it helps you render components at either a shallow level or a deep level while running tests. Again, you may spend time reading about it here.
More about setting up your project with Jest and Enzyme can be learned from the Setting up a section of this article.
Most of how I have described how to write Unit Tests in this series has been inspired by reading this article.
Configuring code coverage
Jest carries code coverage as a part of its huge list of utilities. We only have to configure Jest to generate code review like the way we want it to perform. I have a basic setup for getting the code coverage set up, while a documentation on how to set up in detail can be read here.
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*.{js,jsx}"
],
"coverageDirectory": "/coverage/",
"coveragePathIgnorePatterns": ["/build/", "/node_modules/"],
"setupFiles": [
"/config/polyfills.js"
],
"testPathIgnorePatterns": [
"[/\\\\](build|docs|node_modules|scripts)[/\\\\]"
],
"testEnvironment": node,
"testURL": "http://localhost",
"transform": {
"^.+\\.(js|jsx)$": "/node_modules/babel_jest",
"^.+\\.css$": "/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|css|json)$)": "/config/jest/fileTransform.js"
},
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"
],
"moduleNameMapper": {
"^react_native$": "react-native-web"
}
},
What to test in a React Component
The first thing that ran in my mind when I decided to write Unit Tests is to decide on what to test in this whole component. Based on my research and trials, here is an idea of what you would want to test in your component. Having said that, this is not a rule, your use case may want to test a few aspects out of this list as well.
On a general note, if we are Unit Testing we must be performing tests on a dumb component(simple react component) and not a connected component(component connected to the redux store).
A component connected to the store can be tested both as connected component and dumb component, to do this we have to export the component in our definition as a non-default. Testing connected components is generally an Integration Test
Test what the component renders by itself and not child behavior
It’s important to test all the direct elements that the component renders, at times it might be nothing. Also, Ensure to test the elements rendered by the component that is not dependent on the props passed to the component. This is why we recommend shallow rendering.
Test the behavior of the component by modifying the props
Every component receives props, and the props are sometimes the deciding attributes of how the component would render or interact. Your test cases can pass different props and test the behavior of the component.
Test user interactions, thus testing component internal methods
Components are bound to have user interactions and these user interactions are either handled with the help of props or methods that are internal to the component. Testing these interactions and thereby the components of private methods are essential.
What not to test in a React Component
What to test was simple, and probably something straightforward. We need to be well aware of what not to be tested in a React Component as a part of the Unit Tests.
Do not test PropTypes and Library functions
It does not make sense to test library functions, and those functionalities that you this should be tested as a part of the framework.
Do not test style attributes
Styles for a component tend to change, it does not give any value testing the styles of a component and is not maintainable when styles change the test cases are bound to change.
Do not test default state or state internal to a component
It’s not important to test the state internal to the component, as this is inhibited by default and would get tested indirectly when we test user interactions and methods internal to the component.
Identifying the “Component Contract”
When we start writing test cases for a component, it would make it helpful to decide what to test and what not when we understand the “Component Contract”.
To explain how we identify the contract, let’s discuss with an example.
Consider the following page which is a component called referral jobs.
Here is the code snippet on how this component is written
export class ReferralJobs extends Component {
constructor(props) {
super(props);
this.state ={pageNumber: 1, showReferDialog: false}
}
componentDidMount() {
let data = {"pageNumber": this.state.pageNumber, showReferDialog: false};
this.props.getReferralJobs(data);
}
searchJob = (data) => {
let defaultData = {"pageNumber": this.state.pageNumber};
if(data !== defaultData) {
this.props.getReferralJobs(data);
}
}
handleReferJobDialog = (Job_Posting_ID ,JobTitle) => {
let currentState = this.state.showReferDialog;
this.setState({showReferDialog: !currentState});
this.setState({jobId: Job_Posting_ID});
this.setState({jobTitle: JobTitle});
}
referJob = (data) => {
this.props.referAFriend(data);
}
render() {
return()
Suggested jobs to refer
{this.props.jobs.tPostingJobLists && this.props.jobs.tPostingJobLists.map((job,index)=>{return
{job.JobTitle}
{job.MinExperience} - {job.MaxExperience} Years
{job.State}
{job.ShortDesc}
this.handleReferJobDialog(job.Job_Posting_ID, job.JobTitle)}>Refer a Friend
Share
})}
) } }
const mapDispatchToProps = (dipatch, ownProps) => { return bindActionCreators({ getReferralJobs: getReferralJobsAction, referAFriend: getReferAFriendAction }, dispatch)
const mapStateToProps = (state, ownProps) => { return { jobs: state.referralsState.ReferralJobs, } }
export default connect(mapStateToProps,mapDispatchToProps)(ReferralJobs);
The component is composed of three parts Search, Job Posting Container, and The PortalApp Dialog. Let’s identify the contract for this component and also write test cases.
Search is Always Rendered
The Search Container is always rendered and is not conditional on any of the props, and the search container accepts a prop as its click handler.
This can be divided into two pieces
- Search Container is always rendered no matter what props are being passed to the container
- A click handler is passed as a prop and we need to test if the props have been set
Let’s write a couple of test cases for the same
describe ("ReferralJobs", () => {
let props;
let mountedReferralJobs;
const referralJobs = () => {
if (!mountedReferralJobs) {
mountedReferralJobs = shallow();
}
return mountedReferralJobs;
}
const referralJobsMounted = () => {
if (!mountedReferralJobs) {
mountedReferralJobs = mount();
}
return mountedReferralJobs;
}
beforeEach(() => {
props = {
jobs: {tPostingJobLists: []},
getReferralJobs: jest.fn
};
mountedReferralJobs = undefined;
});
it("Always renders a `ReferralSearch`", () => {
expect(referralJobs().find(ReferralSearch).length).toBe(1);
});
it("sets the rendered `ReferralSearch`'s `onClick` prop to the same value as `getReferralJobs`'", () => {
expect(referralJobsMounted().props().getReferralJobs).toBe(props.getReferralJobs);
});
There are two test cases within a test suite in the above code snippet. Let’s first try to understand how the test suite is configured.
Our describe method initializes a test suite and it can contain any number of test cases within it. It has various functions that it can handle such as after Each, beforeEach etc. read more here.
Within our describe method we have two initializations one called referralJobs and the other as referralJobsMounted. These are two ways in which we can render our component on the Virtual DOM during our testing.
Shallow Rendering
Shallow rendering is a widely used form of rendering when writing Unit Tests with Enzyme, this renders the component at one level deep and does not care about the behavior of child components. This is used when you are bothered about what is rendered in the component of interest.
Mount
Mount is a form of rendering which renders the complete component on the Virtual DOM and also returns an instance of the component. This helps us in cases where we need to test the component level props.
In our above snippet, we are also passing default props to the component so that the ReferralJobs component renders successfully.
Moving to our test cases, we have two test cases like our identified contract, one to verify that the search component is successfully rendered and other to verify that the props set in the component is the prop that we had given it while invoking the component.
As you notice, we use Jasmine’s expect the library to make assertions and this comes built in with Jest.
We pass jobs as props and ‘n’ jobs to be rendered as Paper components
The main functionality of the component is to display jobs that are given to it as props. This can again be separated into two parts.
- ‘n’ jobs have to be rendered, as Paper objects within the Grid Container
- Each job must display the right information on its card
To test this we have two test cases, and we pass a two job object as props in our test case.
describe("when `jobs` is passed", () => {
beforeEach(() => {
props.jobs = {
"tPostingJobLists": [
{
"RowID":1,
"MaxExperience":0,
"MinExperience":0,
"GCI_Rec_ID":0,
"Job_Posting_ID":4215,
"JobTitle":"Java develper -Test",
"Skill_Required":"java oracle",
"City":"",
"State":"Arunachal Pradesh",
"Country":"India",
"Area_Code":"10000",
"ShortDesc":"test descriptuin test descriptuin test descriptuin v test descriptuin test descriptuin test descriptuin vtest descriptuin test descriptuin test descri....",
"shareLink":"http://apps.portalapp.com/jobs/4215-Java-develper--Test-jobs",
"ImagesLink":"http://appst.portalapp.com/eep/Images/app_0007.jpg"
},
{
"RowID":1,
"MaxExperience":0,
"MinExperience":0,
"GCI_Rec_ID":0,
"Job_Posting_ID":4215,
"JobTitle":"Java develper -Test",
"Skill_Required":"java oracle",
"City":"",
"State":"Arunachal Pradesh",
"Country":"India",
"Area_Code":"10000",
"ShortDesc":"test descriptuin test descriptuin test descriptuin v test descriptuin test descriptuin test descriptuin vtest descriptuin test descriptuin test descri....",
"shareLink":"http://apps.portalapp.com/jobs/4215-Java-develper--Test-jobs",
"ImagesLink":"http://appst.portalapp.com/eep/Images/app_0007.jpg"
}
]
};
});
/**
* Tests that the count of Grids rendered is equal to the count of jobs
* in the props
*/
it("Displays job cards in the `Grid`", () => {
const wrappingGrid = referralJobs().find(Grid).first();
expect(wrappingGrid.children().find(Paper).length).toBe(props.jobs.tPostingJobLists.length);
});
/**
* Tests that the first job paper has rendered the right
* job title
*/
On clicking Refer a Friend component state must change
In each job card, we have referred a friend link, on clicking this link the click handler changes the state and a dialog opens.
If you read the component code for ReferralJobs, the dialog has a prop value which is referred to the component state. When this value is true, the dialog is expected to open.
We will need to test if when clicking refer a friend link, the state changes and test whether the dialog prop is set to true or not.
/**
* Test that clicking on refer a friend changes the state of the portal app dialog
* The ReferJob pop up cannot be tested as Enzyme's mount method does not
* render inner components
*
* This illustrates how we can test component level methods
*/
it("opens a `PortalAppDialog` and shows `ReferJob` on clicking on refer a friend link", () => {
const firstJob = referralJobs().find(Grid).first().find(Paper).first();
const referAFriend = firstJob.find('.details-link');
expect(referAFriend.length).toBe(1);
const referDialogBeforeClick = referralJobs().find(PortalappDialog);
expect(referDialogBeforeClick.first().prop('open')).toBe(false);
const referLink = referAFriend.first();
referLink.simulate('click');
const referDialogAfterClick = referralJobs().find(PortalappDialog);
expect(referDialogAfterClick.first().prop('open')).toBe(true);
});
});
In the above snippet we have introduced the simulate functionality. The simulate is a function of enzyme that helps you trigger events on elements.
In our ReferralJobs component code, the click handler for the refers a friend link is an internal component method. Thus, by performing this test we are also testing the internal private method of the component.
Components form the core of a react application. I hope this would have helped you understand how and what to unit test in a component of a react application.
Testing Redux Asynchronous action creators
Assuming that you know what action creators are, let me move to asynchronous action creators. Your actions may make asynchronous HTTP requests to get data, and testing them is important. Action creators are responsible for dispatching the right actions to the redux store.
In our ReferralJobs application, we have used axios to make HTTP requests. You may alternatively use any library(ex: fetch) to make your HTTP requests. In order to test this action creator, we will have to mock the HTTP request and the response. Also, we need to mock the redux store to handle the actions.
For this, we have used redux-mock-store to mock the store and axios-mock-adapter to mock our axios HTTP requests.(If you are using anything apart from axios to perform HTTP request, consider using nock).
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
axios.defaults.baseURL = 'https://appst.portalapp.com/';
const mock = new MockAdapter(axios);
In our test case, we are simply using the libraries and setting up the mocks. We will create a mock store for testing the action creators.
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
axios.defaults.baseURL = 'https://appst.portalapp.com/';
const mock = new MockAdapter(axios);
/**
* Test Suite to test Asynchronous actions
* This test case mocks the API request made by axios with the help of
*/
describe('Testing Async Actions', () => {
afterEach(() => {
mock.reset();
})
it('creates GET_REFERRAL_JOBS when fetching referral jobs has been done', done => {
mock.onPost(getReferralJobsURL).reply(200, { data: { todos: ['do something'] } });
const expectedActions = [
{"type":"FADE_SCREEN"},
{"type":"GET_REFERRAL_JOBS","payload":{"todos":["do something"]}},
{"type":"STOP_FADE_SCREEN"}
]
const store = mockStore({ todos: [], loginState: {accessToken: "sampleAccessToken" }})
store.dispatch(getReferralJobsAction({}));
/**
* Setting timeout as axios and axios-mock-adapter don't work great
* We should move from using axios and use fetch if we don't have a specific reason using
axios
*/
setTimeout(() => {
expect(store.getActions()).toEqual(expectedActions);
done();
}, 1000)
})
})
Once our mockStore is ready, in our test case we mock the HTTP request that is expected to be made. This also gives it a sample response.
When an action creator is called, it is expected to dispatch actions to the store and this is a known object defined as expectedActions in our test case.
We then dispatch the action creator into our store with empty data, now the action creator is expected to make a HTTP request and dispatch necessary actions into the store.
We are testing this post the timeout as the http request is an asynchronous call(this is a hack for getting it working on axios-mock-adapter, if you are using nock the timeout is not required)
Testing Redux Reducers
Testing reducers are straightforward and are not dependent on any setup. Any reducer, when called with an action type, must modify the state and return the state back. Hence we only have to test a state object.
In our test case, we are passing a payload to the action type and we expect the state to change with the appropriate payload.
Redux handles the state of data flow within a react application, writing Unit Tests for its action creators and reducers would bring in a lot of value to your application’s quality. I hope this article helped you.