Testing React Components

React offers (imho) a paradigm shifting technology for client side web applications - or GUIs in general for that matter. If you haven't already; give it a try!

One of the things I really like about React is how it lends itself to TDD and testing in general. Small, focused components relying mostly on props (parameters) are easy to reason about and requires little mock.

The following are some of my experiences testing React components. Ready? Let's go!

EXPEDITION

React Test Utils

React ships with very good test utilities. Unfortunately the documentation is somewhat hidden away on their website. Here is a link.

var React          = require('react')
var ReactAddons    = require('react/addons') // You also need to require the addons
var ReactTestUtils = React.addons.TestUtils  // <- YEAH!

Use jsdom

For any kind of testing to be tolerable, TDD especially, efficient feedback loops are essential. Having to continously pass things off to a browser, or ever worse; multiple browser, is a pain. Luckily we have nodejs & jsdom. The React guys themselves use jsdom for testing.

I like to wrap up jsdom so that it is not required if a document already exists. That way the tests can run both in node and browsers.

UPDATE: There is a module for that (now) :-)

$ vi testdom.js
  module.exports = function(markup) {
      if (typeof document !== 'undefined') return
      var jsdom          = require("jsdom").jsdom
      global.document    = jsdom(markup || '')
      global.window      = document.createWindow()
      // ... add whatever browser globals your tests might need ...
  }

$ vi test/spec.js
  require('../testdom')('<html><body></body></html>')
  console.log(document)

Later we will see how we can hook up our tests to a CI tool for some sweet cross browser coverage.

Avoid JSX

It's up to you, but using jsx introduces an additional build step everywhere without adding much of a benefit. Using the React.DOM javascript API is really straight forward. It'll take you 2 minutes to figure out.

FAIL

UPDATE: I've since changed my mind and are now using jsx. Partly because React introduce some breaking changes that would not have been a PITA if I had been using jsx, and partly becuse it is a nice visual separation of logic and components.

Include a common render function

For each test you want a clean slate. Usually this means rendering the component again. It makes sense wrapping the render code into a function.

var _ = require('lodash') // or similar
var defaultProps = {}

function render(newProps, callback) {
    var props = _.merge(defaultProps, newProps)
    return React.renderComponent(Component(props), document.body, function() {
        if (typeof callback === 'function') setTimeout(callback)
    })
}

I find that keeping a set of defaultProps around makes sense. Callers of render can pass their required props (newProps) and have that merged with defaultProps before rendering. Overwriting the defaults if they want. Since we are testing components in isolation we can usually just mount to document.body. React.renderComponent takes a callback that is called when the component has finished rendering. I found that pushing my render's callback to the next tick of the eventloop (using setTimeout) resulted in a more stable test environment.

Clean up after each test

If you try to render a React component into a DOM which already has react identifiers, React will merge with whatever is already there. Especially when testing the same component over and over your need to clean up your DOM state.

How to do this depends on your test framework. Here is what I do in mocha (tdd interface):

describe('My Component', function() {

    afterEach(function(done) {
        React.unmountComponentAtNode(document.body) // Assuming mounted to document.body
        document.body.innerHTML = ""                // Just to be sure :-P
        setTimeout(done)
    })

    ...tests...
})

We use React.unmountComponentAtNode to unmount the component. Just to be safe we also reset body's innerHTML. I found once again that pushing the callback (done) to the next tick of the eventloop (using setTimeout) created a more stable test suite.

Query the DOM

You can query the DOM directly using the tool of your choice, or you can use the ReactTestUtils to query React components.

it('should render an input', function(done) {
    var _tree = render({}, function() {
        var __input = document.querySelectorAll('input')
        var _input  = ReactTestUtils.findRenderedDOMComponentWithTag(_tree, 'input')
        assert(...)
    })
})

As you might have noticed the findRenderedDOMComponentWithTag (and most other functions of ReactTestUtils) require a ReactComponent parent/tree to query. Luckily we designed our render function to return the top level component.

Simulate events

The ReactTestUtils also let's you simulate events. This is very useful!

it('should do something when I click mySpecialButton', function(done) {
    var _tree = render({}, function() {
        var _button = ReactTestUtils.findRenderedDOMComponentWithClass(_tree, 'mySpecialButton')
        ReactTestUtils.Simulate.click(_button)
        assert(...)
    })
})

For more about the capabilities of ReactTestUtils check out the docs.

Faking XMLHttpRequests

(Not really React specific, but I'll add a note about it anyway.)

Need to fake XMLHTTPRequests? There is a module for that!

var FakeXMLHTTPRequests = require('fakexmlhttprequest')

var requests   = []
XMLHttpRequest = function() { 
    var r =  new fakeXMLHttpRequest(arguments)
    requests.push(r)
    return r
}

describe('My component', function() {

    afterEach(function() {
        requests = [] // <- Reset request pool after each test
        ...
    })

    it('gonna get some data over the wire', function(done) {
        var onDataReceived = function(data) { assert(...); done() }
        render({ onDataReceived : onDataReceived }, function() {
            assert(requests.length > 0)
            requests[0].respond(200, { "Content-Type": "application/json" }, JSON.stringify({...}))
        })
    })

})

Running test

I use mocha solely for the nyancat reporter.

npm install -g mocha

I also find it useful to add my test command to package.json so that I can run my tests consistently with the same command across projects.

$ vi package.json
    ...
    "scripts": {
        "test": "mocha -R nyan -w --check-leaks",
    },
    ...

$ npm test

NYANCAT

Testling

Running the tests in node is convenient and fast, but it is NOT THE SAME as running them in actual browser. So, we need to hook up some actual browser testing too. Testling is a great alternative and free for open source projects. They have great documentation and even a special little guide for using mocha. Plus, you'll get sweet badges:

browser support

There is one little trick I wanted to add though. Testling users browserify to create a browser compatible bundle of your javascripts. Unfortunately jsdom is not compatible with browserify, so we have to tell testling to ignore it.

In your package.json add a browser field and add tell browserify to ignore jsdom.

$ vi package.json

...
"browser" : {
    "jsdom" : false
},
...

Since we, in our jsdom wrapper above, only try to require jsdom if no document exists; the browser will never reach that code and we are good. The tests will use the browser's DOM.

YOU

Now go get some test coverage for your React components!

Credits

♥ to the React guys.
And the nyancat.
And coffee.
And tests.
And gifs.

enjoy!