Root Engineering: Mounting React Native Components with Enzyme and JSDom

Root is a mobile-only insurance carrier. The only way to get a quote, buy insurance, or view policy information is through our iOS app. We recently converted our app from Swift to React Native. One of the most significant motivations for the change was improved testability. Despite experimenting with various architectures in iOS to be able to effectively unit test our code, we felt like we lacked the productive workflow that we enjoyed while working on other platforms. The feedback loop was too slow.

When we first started unit testing scenes in React Native, we used Enzyme. The guide to using Enzyme with React Native recommends using react-native-mock to stub the native calls. This allows running the test suite using Node.js, without any of the native bindings present. We started off using Enzyme’s shallow method for rendering. Unfortunately, the mount and render functions weren’t compatible with React Native. Shallow rendering worked well for getting us started, but it has some drawbacks and limitations.

Shallow rendering doesn’t call the React lifecycle methods. This meant that we had to call those methods ourselves, which was tedious. It also increased the risk that the code would execute differently in production than in our test environment, which is something we always try hard to avoid. Our code would look like this:

const wrapper = shallow(<OurScene />);  
wrapper.instance().componentWillMount();  

A single call to componentWillMount wasn’t so bad, but executing a series of lifecycle methods was much harder.

Shallow rendering also only renders the first level of JSX. It doesn’t render any of the nested components. This meant that if we extracted a component from one of our scenes and wanted to test the integration between the scene and the extracted component, we’d have to do extra work. It ended up looking something like:

const wrapper = shallow(<OurScene />);  
wrapper.instance().componentWillMount();

const componentWrapper = wrapper.find(NestedComponent).shallow();  
componentWrapper.instance().componentWillMount();  

To solve these problems, we wanted to be able to use Enzyme’s mount function. However, enzyme was built for React on the web, not React Native. This is apparent when reading the Enzyme docs and seeing numerous references to the DOM.

Full DOM rendering is ideal for use cases where you have components that may interact with DOM APIs, or may require the full lifecycle in order to fully test the component (i.e., componentDidMount etc.)

Despite being web-based, this is exactly what we wanted: to execute the full lifecycle of components. The recommended approach for using mount without running tests in a browser is to use jsdom.

Full DOM rendering requires that a full DOM API be available at the global scope. This means that it must be run in an environment that at least looks like a browser environment. If you do not want to run your tests inside of a browser, the recommended approach to using mount is to depend on a library called jsdom which is essentially a headless browser implemented completely in JS.

The question we set out to answer: could we get mount+jsdom to mount a React Native component?

We thought that it should be possible. If we could mount our React Native components like they were React for the web components, Enzyme’s mount should work.

We added jsdom to our project and got to work with experimenting. Before long, we had Enzyme mounting our components. However, we noticed most of the native component nodes were missing. After diving into the problem, we realized that react-native-mock was rendering null for the native components. For example, the implementation of a Text node is:

render() {  
  return null;
},

Clearly if we were going to mount our components, we needed to render something instead of null. We started updating render functions to return an element via a call to React.createElement

render() {  
  return React.createElement('react-native-mock', null, this.props.children);
},

After hacking away at react-native-mock for a while to identify all the places that we needed to change, we had it fully working. Rather than stubbing out function calls with empty functions, we updated all of them to render React elements. We thought our changes were substantially different from the core implementation of react-native-mock, so we created a fork and published it as react-native-mock-render.

GitHub: https://github.com/Root-App/react-native-mock-render
npm: https://www.npmjs.com/package/react-native-mock-render

Shout out to Bob Carson from our engineering team for creating the fork and open sourcing the work.

We had to make a few other fixes that are captured in the commit log of our fork: https://github.com/Root-App/react-native-mock-render/commits/master

The end result of our efforts is the ability to mount and execute the full React lifecycle of React Native components.

How to Mount React Native Components with Enzyme

Load up react-native-mock-render in whichever file you use to configure your test suite.

require('react-native-mock-render/mock');  

Then set up jsdom.

 const jsdom = require('jsdom').jsdom;                    
 global.document = jsdom('');                             
 global.window = document.defaultView;                    
 Object.keys(document.defaultView).forEach((property) => {
   if (typeof global[property] === 'undefined') {         
     global[property] = document.defaultView[property];   
   }                                                      
 });                                                      

That’s it. You can now use mount.

Bonus Material: Testing Full Flows using Redux and Navigation

Another goal we had with this effort was being able to mount a series of components to write automated tests that would execute a full flow in the app. We were able to do this by building a fake navigator class. Instead of actually navigating to components, we mount them using jsdom and then maintain a stack of references. In conjunction with Redux, this worked amazingly well to allow us to execute a full flow.

Side note: navigation in React Native is a mess right now with a proliferation of libraries and no clear winner. There’s React Navigation, Native Navigation, React Native Navigation, and more. This following code will need to be adapted to your routing and navigation libraries of choice.

We had to do a little bit of work to wrap navigation calls in the necessary components for Redux, but it wasn’t too hard. The store object passed to the constructor is a Redux store.

export default class MountingNavigator {  
  constructor(store) {
    this.componentStack = [];
    this.store = store;
  }                                                                                

  push(params) {                                                                                 
    const component = this._buildComponent(params);                                                   
    this.componentStack.push(component);                                                         
  }                                                                                              

  _buildComponent(params) {                                                                           
    const router = new Router(this, params.passProps.routes);                                    
    return mount(                                                                                
      <Provider store={this.store}>                                                              
        <params.component {...params.passProps} router={router} />
      </Provider>                                                                                
    );                                                                                           
  }                                                                                              
}                                                                                                

The end result is an easy way to navigate through a series of scenes in our app. We use the page object pattern to drive our UI.

new QuoteSelectDriver(router.currentScene())  
  .selectQuoteTier(Quote.Tier.LOW)          
  .pressContinueButton();                   
new QuoteSummaryDriver(router.currentScene()).pressContinueButton();  
new QuoteStartDateDriver(router.currentScene()).pressContinueButton();  
                                                                                             

We’d love to chat with anybody working on better ways of testing with React Native. Email us at engineering@joinroot.com

Get new posts in your Inbox

Get the app

App store
Play store
ROOT for iPhone