Testing With React
At Venmo, we’ve started using React on our front-end. React is a javascript library for building reactive (dynamic) user interfaces. We use React for our view layer and Backbone for our data layer. Here you can find a blog post by my colleague Thomas Boyt on how we leverage both of these awesome libraries.
Why React?
As our client-side apps grew in size, maintaining and testing our Backbone views became a burden. React ships with JSX
, Javascript XML Syntax Transform, which greatly reduces the amount of complexity that large views incur over time by providing a new way to construct your app’s markup. Backbone views rely on templating and selectors for inserting elements into the DOM
, while React components are declarative.
If you're looking for more information on why you should consider replacing Backbone.View
with React, check out Clay Allsopp's awesome blog post- From Backbone to React.
Here’s an example of how both libraries approach constructing app markup.
// Backbone
define(['./view'], function(View) {
var BackboneView = Backbone.View.extend({
render: function() {
this.$el.html(_.template($('#skeleton_template').html()));
this.$('.container').html(new View().render().el);
return this;
}
});
});
// React
define(['./view'], function(View) {
var ReactComponent = React.createClass({
render: function() {
return (
<div className='container'>
<View />
</div>
);
}
});
return ReactComponent;
});
At first glance, the difference may seem trivial. But over time your views will gain complexity and a simple Backbone view like the one above may end up manually populating several containers, each with its own markup. The React approach is much easier to reason about and moves the concerns of markup construction into the JSX
block.
Testing
Why does this point matter? Well, a test that would have looked like this:
it('renders 3 comments', function() {
var comments = new CommentsCollection(commentFixtures);
var commentList = new CommentsListView({ collection: comments }).render();
expect(commentList.$('.comment').length).to.be(3);
});
Now looks like this:
var TU = React.addons.TestUtils;
it('renders 3 comments', function() {
var commentList = TU.renderIntoDocument(new CommentsComponent({
collection: new CommentsCollection(commentFixtures)
}));
var commentList = TU.scryRenderedDOMComponentWithType(commentList, CommentComponent);
expect(commentList.length).to.be(3);
});
Testing with React allows your tests to be markup agnostic. This means that React is only concerned with whether or not three instances of CommentComponent
exist rather than their markup. The example test written in React is much less brittle because it does not rely on the class name .comment
. Thus, if someone were to swoop through the codebase and change a bunch of class names and DOM
structure, your tests would still make all the proper assertions. This is a massive step towards building a rock-solid app and test suite.
Here’s another example:
var TU = React.addons.TestUtils;
it('renders a FooForm', function() {
var app = TU.renderIntoDocument(new App());
// TU.findRenderedComponentWithType will throw an exception if it’s unable to
// find any children that have type `FooForm`
expect(TU.findRenderedComponentWithType).withArgs(app, FooForm)
.to.throwException();
});
This test is amazing. It simply asserts that our App
component contains a FooForm
component. We don’t care about how FooForm
behaves or what it contains (that’s what spec_foo_form.js
is for); we simply care about its existence.
Conclusion
We’re huge fans of tests. Having a solid test suite has helped us avoid regressions, sleep better, and cool our conscience. In an ideal world, regardless of what library you use, your Javascript app would be composed of many small, modular, extendable and test-covered units. By allowing us to write markup-agnostic tests, React brings us a step forward in our never-ending quest for Javascript testing nirvana.