ReactJS Unit testing
Bài đăng này đã không được cập nhật trong 9 năm
Introduction
Working in a software project, the knowledge of knowing how to write test is a must, because testing is the most powerful tool we know of to improve software quality. Tests reduce bugs, provide accurate documentation, and improve design.
In the previous post I wrote about ReactJS, how does it works and how to implement it in a web application to improve UI, so in this post I will show you how we can write unit test in ReactJS using Jest a unit testing framework developed by facebook to work with ReactJS.
Setup
Jest uses ES2015 features and requires a Node.js version of at least 4.0.0 to run. So before we can begin make sure you have Node.js installed on your local machine. I recommended using NVM to install Node.js. To install both NVM and Node.js just enter the following commands.
$ curl https://raw.githubusercontent.com/creationix/nvm/v0.16.1/install.sh | sh
$ source ~/.bashrc
$ nvm install 4.0
$ nvm use 4.0
After you have installed node next is to install jest and it dependencies. To install jest just enter the following commands.
$ mkdir -p UnitTest/__tests__ && mkdir UnitTest/support && \
mkdir UnitTest/app && cd UnitTest
$ npm init
$ npm install react --save
$ npm install react-dom --save
$ npm install jest-cli --save-dev
$ npm install react-tools --save-dev
$ npm install react-addons-test-utils --save-dev
The first two commands will create a project directory and initialize package.json for us, npm init will be ask you to enter some information, so be sure to follow it instruction. The rest of commands will install packages that will be need for our unit testing.
Now open up package.json and put in the following lines.
// UnitTest/package.json
....
"scripts": [
"test": "jest"
],
"jest": {
"scriptPreprocessor": "<rootDir>/support/preprocessor.js",
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react",
"<rootDir>/node_modules/react-dom",
"<rootDir>/node_modules/react-addons-test-utils"
]
}
....
The line with scriptPreprocessor tells jest to use preprocessor that we will be creating next to transform our script files before processing, because we will be using JSX to create our components.
React is designed to be tested without being mocked and ships with TestUtils to help. Therefore, we use unmockedModulePathPatterns to prevent React from being mocked.
The next step is to create a preprocessor that we've discussed above in order for our code compile successfully. Create a new javascript file in support directory name preprocessor.js and paste in the code below.
// UnitTest/support/preprocessor.js
var ReactTools = require('react-tools');
module.exports = {
process: function(src) {
return ReactTools.transform(src);
}
};
Finally to run the test use command
$ npm test # single run
$ npm test -- --watch # re-run the test when the code has been changed
Jest will run all the files created under _tests_ directory.
Creating sample component
Now that we have our environment setup it's time to write some unit test. But before we begin to write test we need something to test with right? So lets implement a react component that will render a dynamic foods list with a form that will let the users add their favourite foods to the list. I'll call this component ListView which will be compose with two more components ListBox and ListForm.
// UnitTest/app/ListBox.js
var React = require('react');
var ListBox = React.createClass({
render: function() {
var liNodes = this.props.foods.map(function(food, index) {
return (<li key={index}>{food}</li>);
});
return (<ul>{liNodes}</ul>);
}
});
module.exports = ListBox;
// UnitTest/app/ListForm.js
var React = require('react');
var ListForm = React.createClass({
getInitialState: function() {
return {food: ''};
},
setFood: function(e) {
this.setState({food: e.target.value});
},
addNewFood: function(e) {
e.preventDefault();
if (!this.state.food) return;
this.props.onNewFoodAdded(this.state.food);
this.setState({food: ''});
},
render: function() {
return (
<form onSubmit={this.addNewFood}>
<input
type="text"
className="txtFood"
placeholder="Name of the food"
value={this.state.food}
onChange={this.setFood}
/>
<input type="submit" className="btnAdd" value="New Food" />
</form>
);
}
});
module.exports = ListForm;
// UnitTest/app/ListView.js
var React = require('react');
var ListBox = require('./ListBox');
var ListForm = require('./ListForm');
var ListView = React.createClass({
getInitialState: function() {
return {foods: []};
},
handleNewFoodAdded: function(food) {
var foods = this.state.foods;
foods.push(food);
this.setState({foods: foods});
},
render: function() {
return (
<div className="listView">
<h1>List of foods</h1>
<ListBox foods={this.state.foods} />
<ListForm onNewFoodAdded={this.handleNewFoodAdded} />
</div>
);
}
});
module.exports = ListView;
I won't go into details of what this code does, so if you want to know what it means I suggest you to go back and read my previous post about ReactJS here.
Write unit test
Our goal is to test list view get updated or not when user enter and submit his/her favourite food. The three lines with jest.dontMock is really self explainatory right? Basically what it does is tell jest not to mock the code in file which is pass in as argument. Since we want to test components that has been implemented in these files it make sense not to mock it. Next we require all necessary components that need to write test.
// UnitTest/__tests__/ListView-test.js
jest.dontMock('../app/ListView.js');
jest.dontMock('../app/ListBox.js');
jest.dontMock('../app/ListForm.js');
var React = require('react');
var TestUtils = require('react-addons-test-utils');
var ListView = require('../app/ListView');
....
To get these tests running you need to create an instance of the component and render it into the virtual DOM. We do this with the following code.
// UnitTest/__tests__/ListView-test.js
describe('ListView', function() {
var ListViewNode;
beforeEach(function() {
ListViewNode = TestUtils.renderIntoDocument(<ListView />);
});
....
});
The final piece of the puzzle is to add the expectations to the test. The TestUtils.scryRenderedDOMComponentsWithTag method find the elements with the tag name pass in as second argument in the DOM tree pass in as first argument and return array if elements was found. Here we don't have any li in the DOM tree yet so we expected array to have zero element. Next we input the value into textbox by using TestUtils.Simulate.change method, the first argument is the DOM that we want to change and the second argument the value we want to change. Note that I pass object as second argument instead of strin,g the reason is because this method was use for general element not just for textbox. After input some value into textbox next is to submit the form and we do that by using TestUtils.Simulate.submit method. And lastly we try to find li tag in the DOM tree again and expected it to return array contains one element which means the DOM tree did updated reacting to the user adding a new food name to the list.
// UnitTest/__tests__/ListView-test.js
....
describe('when "New Food" has been click', function() {
it('should add new food to as li tag to listbox', function() {
var items = TestUtils
.scryRenderedDOMComponentsWithTag(ListViewNode, 'li');
expect(items.length).toEqual(0);
TestUtils.Simulate.change(
TestUtils.findRenderedDOMComponentWithClass(ListViewNode, 'txtFood'),
{target: {value: 'food 1'}}
);
TestUtils.Simulate.submit(
TestUtils.findRenderedDOMComponentWithTag(ListViewNode, 'form')
);
items = TestUtils
.scryRenderedDOMComponentsWithTag(ListViewNode, 'li');
expect(items.length).toEqual(1);
});
});
....
Try running the test with npm test and you'll see the test passed.
Last word
This post act as the introduction to writing unit test for ReactJS only. For the complete reference you can checkout the official facebook documentation which can be found here and also checkout Jamsime for the complete reference to mock and expectation methods.
All rights reserved