React native
It’s a framework which allows the use of Javascript and React to build native applications. Support for iOS was there from the beginning but building for Android platform was added only weeks ago and it still needs attention from the authors to cover the same functionality. React-native is a breath of fresh-air, even if the idea of writing an application code once and then building it for multiple mobile environments is not that new.
Reusing tools
Transition from react-web applications to react-native application is very smooth. Framework is built on already existing ecosystem known from web development and, of course, react itself. You may expect that your favourite npm package will work here. If you think about Jest testing framework, that will be true as well. Kind of.
Getting started
Conflicts
Jest issue #340 forced me to lock jest-cli
dependency on version 0.5.4
. Making strict version requirement for jest-cli
dependency is straightforward, make sure your package.json contains such line:
"jest-cli": "0.5.4"
ES Features
Moreover, the code of our application will be translated to old’n’good ecmascript5 using babel. Unfortunately the white list of supported features is hardcoded inside react-native transformer and there is no merge option, only override choice. The original list for current version (0.11.4) can be found here.
I like es6 modules system, so I need es6.modules
transformer on the whitelist property. To add it, I need to copy the list from react-native source to my .babelrc
and add missing transformer, like this:
{ "whitelist": [ "es6.arrowFunctions", "es6.blockScoping", "es6.classes", "es6.destructuring", "es6.modules", "es6.parameters", "es6.properties.computed", "es6.properties.shorthand", "es6.spread", "es6.templateLiterals", "es7.asyncFunctions", "es7.trailingFunctionCommas", "es7.objectRestSpread", "flow", "react", "react.displayName", "regenerator" ] }
This file will be found by react-native packager, but not by Jest. To make it work we need to add babel transformer for test scripts:
- install
babel-core
package
npm install babel-core --save-dev
- add custom preprocessor to package.json
"jest": { "scriptPreprocessor": "jestSupport/scriptPreprocess.js" }
- add code of preprocessor
jestSupport/scriptPreprocess.js
var babel = require('babel-core'); module.exports = { process: function (src, filename) { var result = babel.transform(src, { filename: filename }); return result.code; } };
That’s it. Now we use the same .babelrc
configuration for packager and during tests.
Rendering
React-native package doesn’t play nicely when loaded in a Jest test. The good thing is that we can replace it with react. Naturally, we need to install react as dependency:
npm install react --save-dev
Now to mock the native library, create react-native.js
file in mocks folder with the following content:
var React = require('react/addons'); var ReactNative = React; module.exports = ReactNative;
From now on our tests will throw exceptions on missing constructors or static methods, and that’s because react
doesn’t contain native-only classes like View
, Text
or Stylesheet
. We need to mock them as well, but do we really need to reimplement the whole native collection using html just to run the tests with them? That sounds like way to maintenance hell. Fortunately we have an experimental shallow rendering feature of TestUtils
.
Shallow rendering outputs a virtual tree, it doesn’t even try to compose html or native objects. This way we can inspect how the component would be built, but without actually rendering it. Perfect!
Let’s get rid of the warning, but by adding very naive implementation of native components:
var React = require('react/addons'); var ReactNative = React; ReactNative.StyleSheet = { create: function(styles) { return styles; } }; //Yup, quite naive class View extends React.Component {} class Text extends React.Component {} ReactNative.View = View; ReactNative.Text = Text; ReactNative.TouchableWithoutFeedback = View; module.exports = ReactNative;
Now shallow rendering works without any warning:
const shallowRenderer = TestUtils.createRenderer(); class MyComponent extends View { render() { return (<Text>Hello!</Text>); } } shallowRenderer.render(<MyComponent>Hello</MyComponent>); let output = shallowRenderer.getRenderOutput(); console.log(output); /* Object { type: [Function: Text], key: null, ref: null, _owner: null, _context: Object {}, _store: Object { props: Object { children: 'Hello!' }, originalProps: Object { children: 'Hello!' } } } */
Assertions
With such output we can now traverse though the virtual tree, but that means we need to know the exact structure of the component. For the purpose of simple tests this might be enough:
it('should render Text node', function () { shallowRenderer.render(<MyComponent>Hello</MyComponent>); let output = shallowRenderer.getRenderOutput(); expect(output.type).toBe(Text); });
However, for a more complex tree it’s convenient to have some helpers, like react-shallow-renderer-helpers. Jest uses Jasmine under the hood, so we can take the advantage of custom matchers for cleaner specs code. I’ve found a solution described in this article to be a good base, I’ve even reimplemented the sample matcher using shallowHelpers.filterType
method here.
After applying the render helpers and custom matchers, spec file looks like:
it('should render Text node', function () { shallowRenderer.render(() => <MyComponent>Hello</MyComponent>); let output = shallowRenderer.getRenderOutput(); expect(output).toContainReactNodeInTreeLike(<Text />); });
Additionally, the render helper gives access to the component instance and lets us test internal state:
class MyComponent extends View { constructor(props) { super(props); this.state = { name: 'World' } } someEventHandler(name) { this.setState({ name }); } render() { return (<Text>Hello {this.state.name}!</Text>); } } describe('MyComponent', function() { it('should render user name', () => { shallowRenderer.render(() => <MyComponent />); let instance = shallowRenderer.getMountedInstance(); let output; output = shallowRenderer.getRenderOutput(); expect(output).toContainReactNodeInTreeLike(<Text>Hello {'World'}!</Text>); instance.someEventHandler('Jack'); output = shallowRenderer.getRenderOutput(); expect(output).toContainReactNodeInTreeLike(<Text>Hello {'Jack'}!</Text>); }); });
By default, Jest hides the details of test suites. To see a nice tree of tested features we need to run cli with --verbose
option, for example by configuring test
script in package.json:
"scripts": { "test": "jest --verbose" }
All together
Check out this repository to see everything above put together in a sample project.