It’s going to be a story about tackling problems on the way to run unit tests using react-native. It’s one of these stories when we don’t even know what is the question, so it’s difficult to ask Google. It’s a story of how to test react-native components with Jest.

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.

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" 1 "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" ] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "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 1 npm install babel - core -- save - dev

add custom preprocessor to package.json

"jest": { "scriptPreprocessor": "jestSupport/scriptPreprocess.js" } 1 2 3 "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; } }; 1 2 3 4 5 6 7 8 9 10 11 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 1 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; 1 2 3 4 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; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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!' } } } */ 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 const shallowRenderer = TestUtils . createRenderer ( ) ; class MyComponent extends View { render ( ) { return ( < Text > Hello ! < /Text>); } } shallowRenderer.render(<MyComponent>Hello</M yComponent > ) ; 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); }); 1 2 3 4 5 6 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 />); }); 1 2 3 4 5 6 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>); }); }); 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 33 34 35 36 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" } 1 2 3 "scripts" : { "test" : "jest --verbose" }

All together

Check out this repository to see everything above put together in a sample project.