SuperPumpup (dot com)
General awesomeness may be found here.

08 January 2016

React Native & Jest Testing

Objective:

Have a way of testing my RN components with Jest that Doesn't Suck.

Background:

There exists a reasonable corpus of knowledge around testing React components with Jest. React is relatively new, Jest is newer, and though each have documentation, their interop has some nuance. Then, React Native gets added to the mix with a whole 'nother set of nuances and things kind of get crazy. So I spent a few days trying to figure out something reasonable.

This example is of some components whose render functionality will be tested. The project uses Redux, so there is not much store interaction, and a lot of passing of big prop trees through components. It shouldn't matter too much to the testing, but knowing that may give more context.

High Level Strategy:

We're going to use ReactTestUtils' shallowRender function to generate a "one" (really two) layer virtual DOM tree to inspect, then do some assertions on that. The object we are inspecting never actually gets written to HTML, and we are not doing "real" HTML DOM test/inspect. Rather we are just inspecting the objects that React Native is going to paint to a screen (which, being RN, is not a web browser), and can make sure our logic is working based on that.

In order to do this we need a convenient way to "render" a component to something we can inspect, and we need some tools for making assertions about that result.

Execution

Composing

Setup

I'll start with the "header" of my test file.

'use strict';

jest.autoMockOff();

const shallowHelpers = require('react-shallow-renderer-helpers');
const findMatchingType = require('./findMatching').findMatchingType;
const objectAssign = require('object-assign');

import React, { View, Text } from 'react-native';

const NewLoan = require('../NewLoan');
const NewLoanForm = require('../NewLoanForm');
const PrepareSchedule = require('../PrepareSchedule');
  • jest.autoMockOff() - is important because if I don't have that, one of the components in my NewLoanForm object I include later will blow up. It's a bit of a liability, but not something that I've gone after fixing yet.
  • const shallowHelpers = require('react-shallow-renderer-helpers'); - this pulls in a bunch of React functionality for testing component trees (which is totally useless to us since RN components wind up being structured pretty differently from React components), but does pull in some nice functionality wrapping the shallowRender function.
  • var findMatchingType = require('./findMatching').findMatchingType; - this is a tool I built for doing assertions in the next section.
  • const objectAssign = require('object-assign'); this is useful for having our "default" props and being able to add new ones per-test. You could get similar functionality from lodash or similar, but I would prefer to keep that dependency out of my projects until necessary.

  • import React, { View, Text } from 'react-native'; - notice a couple of things here. First, this is the ES6 import syntax, where everything else uses require. This is because 1) I much prefer this syntax and 2) if I try using this syntax for the other lines, the autoMockOff(); won't work and these would be mocked - even if I ask for them not to be. This is due to JS hoisting, and may be fixed in the near future. The module that's being imported is a mock that will be detailed elsewhere.

  • const NewLoan, const NewLoanForm, const PrepareSchedule - these are not mocked and required in for doing assertions. I'm wanting to assert that when I render NewLoan with certain props, then it will have a NewLoanForm as a child, whereas if I give it different props, it will render a PrepareSchedule as a child. It may be nice to just do the comparison with string names, but this is good for now.

Render

It's nice extract a reusable render method for your components that you will be testing in different state, with different props, etc.

describe('NewLoan', () => {
  let newLoan;

  function renderNewLoan(props) {
    const defaultProps = {
      isFetching: false,
      loans: {
        preparedLoan: null,
        preparedSchedule: null,
        form: {
          fields: {},
          isFetching: false
        }
      }
    };
    const testProps = objectAssign(defaultProps, props)

    const shallowRenderer = shallowHelpers.createRenderer();
    shallowRenderer.render(() => <NewLoan {...testProps}/>);
    const output = shallowRenderer.getRenderOutput();

    return {
      props,
      output,
      shallowRenderer
    };
  }
  // Your assertions will go here
}

So I have a set of defaultProps that the NewLoan view will generally depend on to make its rendering decisions.

This defaultProps object then gets objectAssignd (merged, basically), with the provided props.

A renderer is constructed, the component rendered into it, and the output returned in an object.

Note that

return {
  props,
  output,
  shallowRenderer
};

Will really return an object like:

return {
  props: props,
  output: output,
  shallowRenderer: shallowRenderer
};

(thanks ES6 - and Obama).

Now let's render a component:

it('should display the NewLoanForm if there is no loan prepared', () => {
  const testProps = {};

  newLoan = renderNewLoan(testProps);
  const { output } = newLoan;
});

Sweet! We have something to look at. What is this output thing? console.log tells us:

Object {
  '$$typeof': Symbol(react.element),
  type: [Function: View],
  key: null,
  ref: null,
  props:
   Object {
     style: Object { flexDirection: 'column', flex: 1, width: 300, marginTop: 30 },
     children: [ [Object], [Object] ] },
  _owner: null,
  _store: Object {}
}

What I'm going to be asserting on generally is the type, props, and children - more likely the props of children when changing component state (though since this is Redux, MOST MOST MOST state should wind up being in the main state object and components only care about props & actions).

I was so excited about having this to assert on that my first tests when I got to this point were things like:

expect(output.props.children[0].props.children[0].type.displayName).toEqual('NewLoanForm');

I pasted that into our development Slack channel and immediately lost the respect of most of our engineering team. Well, at least that's my deepest fear. I'm sure they still love me. There were some (very correct) criticisms, and it was clearly time to move on to step 2. Making decent assertions.

Asserting

Gotchas

I'm not sure where this fits in the flow of this post, but I should mention something horrifying that I found. If you look at the assertion: expect(output.type.displayName).toEqual('NewLoanForm'); you may think, "Cool, it seems my React components have a property called DisplayName within their type that I can test against. Thanks React!" And you may even test against that for a little while and it will work. But then you will do that for a new component and get this result:

- Expected: undefined toEqual: 'NewLoan'

Wut?

It turns out that if you declare a component using the syntax: var NewLoan = React.createClass({}), JSX will helpfully add a property displayName to the component. If you do: export default class NewLoan extends Component {}, then no such luck. You have no displayName. I would like that day of my life to figure all that out back please.

Ok, Assertions For Real

There is a nice module for doing assertions against React components generated by shallowRender - https://github.com/sheepsteak/react-shallow-testutils. Sadly, almost none of the matchers (isComponentOfType, findAllWithClass, etc.) work in RN because a RN component is pretty different from a React component. Its findAll function does work pretty well, though (and it seemed kind of magical until I looked through it and realized it was just like an interview-question-type tree traversal).

Fortunately, this is all just JavaScript, and you can make your own matchers. These are what I came up with:

find[All]MatchingType

This will look for an element that matches what you're looking for. Simple. The All variant returns the array, the non-all variant just blows up if you don't have 1 exactly.

export function findAllMatchingType(tree, match) {
  return findAll(tree, (el) => {
    const typeMatch = match.type ? el.type === match.type : true;

    return typeMatch;
  }
  );
}

export function findMatchingType(tree, match) {
  const found = findAllMatchingType(tree, match);
  if (found.length !== 1) throw new Error('Did not find exactly one match');
  return found[0];
}

It should be noted that the type attribute that you are looking at here is sometimes friendly, sometimes a big nasty function. If you do the export default class... syntax, it has one form:

function PrepareSchedule() {
  _classCallCheck(this, PrepareSchedule);

  _get(Object.getPrototypeOf(PrepareSchedule.prototype), 'constructor', this).apply(this, arguments);
}

Otherwise, it can be:

function (props, context, updater) {
  // This constructor is overridden by mocks. The argument is used
  // by mocks to assert on what gets mounted.

  if (process.env.NODE_ENV !== 'production') {
    process.env.NODE_ENV !== 'production' ? warning(this instanceof Constructor, 'Something is calling a React component directly. Use a factory or ' + 'JSX instead. See: https://fb.me/react-legacyfactory') : undefined;
  }

  // Wire up auto-binding
  if (this.__reactAutoBindMap) {
    bindAutoBindMethods(this);
  }

  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;

  this.state = null;

  // ReactClasses doesn't have constructors. Instead, they use the
  // getInitialState and componentWillMount methods for initialization.

  var initialState = this.getInitialState ? this.getInitialState() : null;
  if (process.env.NODE_ENV !== 'production') {
    // We allow auto-mocks to proceed as if they're returning null.
    if (typeof initialState === 'undefined' && this.getInitialState._isMockFunction) {
      // This is probably bad practice. Consider warning here and
      // deprecating this convenience.
      initialState = null;
    }
  }
  !(typeof initialState === 'object' && !Array.isArray(initialState)) ? process.env.NODE_ENV !== 'production' ? invariant(false, '%s.getInitialState(): must return an object or null', Constructor.displayName || 'ReactCompositeComponent') : invariant(false) : undefined;

  this.state = initialState;
}

There doesn't seem to be anything in there that makes it seem like it shuold be the right component, but if I compare it with a "dummy" component:

var FooClass = React.createClass({
  render() {
    return <View />
  }
})

It does not match. Who'da thunk.

find[All]Matching

This matcher will try to match both the type of the element and the props. It's a trivial extension of the previous matcher:

export function findAllMatching(tree, match) {
  return findAll(tree, (el) => {
    const typeMatch = match.type ? el.type === match.type : true;
    const propsMatch = objectMatches(match.props, el.props);

    return typeMatch && propsMatch;
  }
  );
}

export function findMatching(tree, match) {
  const found = findAllMatching(tree, match);
  if (found.length !== 1) throw new Error('Did not find exactly one match');
  return found[0];
}

I have extracted those modules into a file on my filesystem, and will probably extract it out to a react-native-shallow-testutils or similar as it gets more robust.

Putting it all together

This is a full test case:

it('should display the NewLoanForm if there is no loan prepared', () => {
  const testProps = {};

  newLoan = renderNewLoan(testProps);
  const { output } = newLoan;

  const match = findMatchingType(output, <NewLoanForm />);

  expect(match).toBeTruthy();
});

There we go. The contents of match in this case are the NewLoanForm component instance that got rendered, but mostly all I'm checking at THIS level of testing is whether by "default" it will render a form. In other tests I vary the props to have it render other things (more loan info collection, confirmation view, submission successful view, etc)

Conclusion

This has been hard. I'm pretty good at the internet, and still, it's been very hard. I'm very grateful to Facebook for getting this great tech out there, and really look forward to watching these projects evolve.

Resources:

  • http://www.asbjornenge.com/wwc/testing_react_components.html
  • http://www.schibsted.pl/2015/10/testing-react-native-components-with-jest/ • I did not like that this uses the shallowRender technique, and tried to avoid it. However, @cpojer suggested that that is a good way to test components
  • https://jamesfriend.com.au/better-assertions-shallow-rendered-react-components
  • Reactiflux Discord - #flux
Categories: Software