Unit Testing React-Native Components with Enzyme Part 1
When I fist started writing “real” apps in Javscript everyone ware writing about the importance of testing, but nobody showed me how. I saw a lot of articles that showed simple tests for out of context functions, components etc. but no step by step guide to following the TDD or BDD way of doing things.
This post is what I would like to find when I first started with React-Native - step by step approach to creating an app with test-first mentality.
In this post I will be using Enzyme instead of Jest used by Facebook. By doing so I can stick with the tools I know and like, namely mocha and chai.
From Enzyme website:
Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components’ output.
Enzyme is unopinionated regarding which test runner or assertion library you use, and should be compatible with all major test runners and assertion libraries out there.
This post is based on excellent Github Gist by Justin Reidy
Project setup
Let’s create a demo project using react-native-cli:1
2
3npm install -g react-native-cli
react-native init enzyme_demo
cd enzyme_demo
For the dev dependencies we need:1
npm install --save-dev mocha chai enzyme
Babel install
Because we will be using ES6 in this post we also need to install and configure babel:
1 | npm install --save-dev babel-cli babel-preset-es2015 babel-preset-react |
Now we just need to tell babel which presets we want it to run. The preset configuration can be placed in package.json or .babelrc. For a small project like this I prefer to do in the former.
So let’s add the following section to the end of our package.json configuration object:1
2
3
4
5
6
7
8
9"babel": {
"presets": [
"es2015",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
}
Project folder structure
To keep all the project files I created 2 folders:
- components - to keep all my component source files
- test - to keep my test / spec files
The tree structure should look like this:1
2
3
4
5
6
7
8
9
10▸ android/
▸ components/
▸ ios/
▸ node_modules/
▸ test/
.flowconfig
.watchmanconfig
index.android.js
index.ios.js
package.json
Unit Tests
Finally that we have most of our initial setup ready (there’s more to come) we will write our first test (following TDD principles).
To make the post a little more interesting and to show few pitfalls for React-Native (and React) component testing we will create a simple keypad with a display.
Our first test will be be in file test/keypad.spec.js1
2
3
4
5
6
7
8
9import { shallow } from 'enzyme';
import { expect } from 'chai';
describe('<Keypad />', () => {
it('should at least run this test', () => {
expect(true).to.be.ok;
});
});
If we try to run it: mocha --compilers js:babel-register
we will get the following error:1
Error: Cannot find module 'react-dom/server'
This is because enzyme was designed as a React testing framework and react-dom and react-addons-test-utils to function properly. Let’s add them:
1 | npm install --save-dev react-dom react-addons-test-utils |
We will also need to add React reference on top of the test file:1
import React from 'react';
If we run the test again it passes which means we have the basic React testing configuration ready.
Now let’s make sure that we can render our Keypad component, which will contain elements wrapped in
1 | it('should render <Keypad />', () => { |
If we run the test using mocha command above we will get an error that Keypad is not defined. It’s to be expected because we didn’t create the component yet. Lets create a minimal implementation of Keypad just to satisfy the test:
We create a new file components/keypad.js with the following content:
1 | import React from 'react-native'; |
If we now reference the Keypad at the top of the file and run mocha we will get the following error:1
2
3
4
5/node_modules/react-native/Libraries/react-native/react-native.js:112
...require('React'),
^^^
SyntaxError: Unexpected token ...
This is because our component is requiring react-native which enzyme does not support out of the box - we need to mock it somehow.
The easiest and most extensible way is to create mocha compiler for .js files like so:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32var fs = require('fs');
var path = require('path');
var babel = require('babel-core');
var origJs = require.extensions['.js'];
require.extensions['.js'] = function (module, fileName) {
var output;
// check if we are loading react-native, if so replace it with a mock file
if (fileName.indexOf('/node_modules/react-native/Libraries/react-native/react-native.js') > -1) {
fileName = path.resolve('./test/support/mocks/react-native.js');
}
// don't transpile files from node_modules
if (fileName.indexOf('node_modules/') >= 0) {
return (origJs || require.extensions['.js'])(module, fileName);
}
var src = fs.readFileSync(fileName, 'utf8');
output = babel.transform(src, {
filename: fileName,
sourceFileName: fileName,
// load transform that will allow us to use spread operator in our mock
plugins: [
"transform-object-rest-spread"
],
presets: [ 'es2015', 'react'],
sourceMaps: false
}).code;
return module._compile(output, fileName);
};
Ok, but what’s in this magic mock file that we are loading?
Well, here it is:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import React from 'react';
function mockComponent(type) {
const Component = React.createClass({
displayName: type,
propTypes: { children: React.PropTypes.node },
render() { return React.createElement(React.DOM.div, this.props, this.props.children); },
});
return Component;
}
const componentsToMock = [
'View',
'Text',
'Component',
'ScrollView',
'TextInput',
'TouchableHighlight',
];
export const MockComponents = componentsToMock.reduce((agg, type) => {
agg[type] = mockComponent(type);
return agg;
}, {});
export default {
...React,
...MockComponents,
StyleSheet: {
create: (ss) => ss,
},
PropTypes: React.PropTypes,
};
Basically what we are doing is creating a simple implementation for every component name on the componentsToMock list.
Lets add test command to our package.json so we don’t need to write all the mocha parameters all the time.
Our package.json should look like so:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29{
"name": "enzyme_demo",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "mocha --compilers js:./test/support/compiler.js",
"test:watch": "mocha --compilers js:./test/support/compiler.js --watch"
},
"dependencies": {
"react-native": "0.18.1"
},
"devDependencies": {
"babel-cli": "6.4.5",
"babel-preset-es2015": "6.3.13",
"babel-preset-react": "6.3.13",
"chai": "3.5.0",
"enzyme": "1.4.1",
"mocha": "2.4.5",
"react-addons-test-utils": "0.14.7",
"react-dom": "0.14.7"
},
"babel": {
"presets": [
"es2015",
"react"
]
}
}
Now when we run npm test
(or event better npm run test:watch
for continuous testing) all our tests pass.
Let’s make our tests fail by creating another test :D. We need to have 10 (0-9) number buttons - so let’s make sure they are there :)
1 | it('should have 10 number buttons', () => { |
For now we will satisfy the test by creating a minimal implementation of KeypadButton (components/keypad-button.js):
1 | import React from 'react-native'; |
Also we will add 10 buttons to our keypad.js:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import React from 'react-native';
let {
View,
Component
} = React;
import KeypadButton from './keypad-button.js';
class Keypad extends Component {
constructor(props){
super(props);
}
render() {
return (
<View>
<KeypadButton/>
<KeypadButton/>
<KeypadButton/>
<KeypadButton/>
<KeypadButton/>
<KeypadButton/>
<KeypadButton/>
<KeypadButton/>
<KeypadButton/>
<KeypadButton/>
</View>
)
}
}
export default Keypad;
Our test pass now - which is great, but we are far from finished.
In the next part we will add numbers to the buttons (of course tests will come first) and look at testing touch events while still using shallow rendering.
Comments