Building Trusted React Web Applications: Part 3, Testing with Jasmine

Original author: Matt Hinchliffe
  • Transfer
  • Tutorial
Translation of the article “Building robust web apps with React: Part 3, testing with Jasmine”, Matt Hinchliffe

From the translator: this is the translation of the third part of the series of articles “Building robust web apps with React”
Translations:


In the second part, I covered the optimization process of my Tube Tracker browser application , but every change I make still requires a browser update to verify that everything works. The application seriously requires a set of tests to speed up the development process and avoid code regression. As it turned out, this is easier said than done when you start working with new technology like React.


Test setup


I use the Jasmine test framework , as it is easy to install and widely used, including in the React library. The application now contains a folder testwith two directories; in the libscripts folder for running tests and the folder specin which the tests themselves are located:

tube-tracker /
├── app /
├── public /
└── test /
    ├── lib /
    │ └── jasmine-2.0.0 /
    ├── spec /
    │ ├── common /
    │ ├── component /
    │ ├── bundle.js
    │ └── suite.js
    └── SpecRunner.html


In addition to the development and production environments that I described in the previous part , I added a test environment in order to bundle the application and the tests. To do this, I included all the test files (specs) in the file suite.jsand used it as an entry point for Browserify:

$ browserify -e test/spec/suite.js -t reactify -o test/spec/bundle.js

Creating a test environment can be improved with some additional automation, but the underlying process already works. The ease of installation also means that the tests run in the browser, and not in a special environment, such as jsdom , which I prefer.

Note: I switched from using Bower to the NPM React distribution. In the Bower version of React, testing utilities and other add-ons are supplied with the library core, which means that the kernel can be turned on twice in the test environment. This causes conflicts between components declared in different packages. Using the NPM distribution allows Browserify to build each package only with the dependencies it needs, avoiding duplication.

Testing React Components


If we consider that React is V (representation) in MVC, then, theoretically, only component output should be tested, but React components often contain logic for processing dynamic behavior, and simple applications can only consist of them. For example, inside the components of the Tube Tracker application contains logic for validating user input, setting an AJAX pool (poll), and displaying status. Therefore, testing a single output will not provide enough information if something breaks inside, so testing an internal implementation is also necessary.

React Test Tools

To make testing React components a little easier, React developers have provided testing tools (TestUtils). An add-on that is likely to be the first one you find by looking for information on testing React applications. It can be used by connecting the React package with add-ons to the test files. The namespace React.addons.TestUtilscontains methods for simulating events, selecting by components, and testing their types.

There is a very useful method renderIntoDocumentthat can render components into an anonymous DOM node, but for some tests it remains to specify a container, for example, to capture events or test the component's life cycle when it is destroyed:

describe("A component", function() {
  var instance;
  var container = document.createElement("div");
  afterEach(function() {
    if (instance && instance.isMounted()) {
      // Only components with a parent will be unmounted
      React.unmountComponentAtNode(instance.getDOMNode().parent);
    }
  });
  describe("rendered without a container reference", function() {
    beforeEach(function() {
      // This component does not use any lifecycle methods or broadcast
      // events so it does not require rendering to the DOM to be tested.
      instance = TestUtils.renderIntoDocument();
    });
    it("should render a heading with the given text", function() {
      // TestUtils provide methods to filter the rendered DOM so that
      // individual components may be inspected easily.
      var heading = TestUtils.findRenderedDOMComponentWithTag(instance, "h1");
      expect(heading.getDOMNode().textContent).toBe("Hello World");
    });
  });
  describe("with a container reference required", function() {
    beforeEach(function() {
      // This component broadcasts events and has lifecycle methods
      // so it should be rendered into an accessible container.
      instance = React.renderComponent(, container);
      this.eventSpy = jasmine.createSpy();
      container.addEventListener("broadcast", this.eventSpy, false);
    });
    afterEach(function() {
      container.removeEventListener("broadcast", this.eventSpy, false);
    });
    it("should broadcast with data when component is clicked", function() {
      // TestUtils can simulate events
      TestUtils.Simulate.click(instance.getDOMNode());
      expect(this.eventSpy).toHaveBeenCalledWith("some", "data");
    });
  });
});

TestUtils greatly simplifies the interaction and testing of component output, but this does not concern the study of their internal implementation.

Component implementation research

Representations of applications, if you work according to the MVC pattern, do not contain any logic, not counting several cycles or conditions, all the rest of the logic should be submitted to the presenter. React applications do not fit into this model, components can themselves be small applications, and some of their insides need to be investigated.

image
The Tube Tracker application contains components up to four levels of nesting, and most of the application logic is inside them.

You will not go far in trying to test all methods of components, since in spite of the fact that methods can be called, you cannot modify them, at least without digging into the interns of React . Thus, the installation of stubs and mobs will not work, which at first may seem like a problem.

The solution is not to create blind spots for testing. If you start to feel that some piece of logic that does not directly affect the output should be available for testing, abstract this code. The external logic of the component can thus be isolated.

Isolating CommonJS Modules

We need to test each module in isolation, since working with the entire component tree can be ineffective in debugging errors and leads to the fact that the tests do not work completely independently. The problem is that CommonJS modules create their own scope and only their public properties can be accessed from dependent components. This poses a problem with testing, since module dependencies are not always declared public. For example, in the Tube Tracker application, the component tube-tracker.jscontains dependencies network.jsand predictions.js:
 
/** @jsx React.DOM */
var React = require("react");
var Predictions = require("./predictions");
var Network = require("./network");
var TubeTracker = React.createClass({
  render: function() {
    return (
      
); } }); module.exports = TubeTracker;

To get around the lack of visibility, I can modify the modules so that their dependencies are supplied to them from the outside, instead of being created inside them, this is a basic dependency inversion (IoC) pattern . Without some kind of dependency injection , using the IoC pattern can lead to spaghetti dependencies. But dependency injection is not a very popular thing in JavaScript projects, since it requires strict adherence to conventions, and its implementation can be very different .

Fortunately, there are many simpler ways to penetrate and replace CommonJS modules. There is Rewire for node.js , the browser version of this tool can be built by transformationRewireify available for Browserify:

$ npm install --save-dev rewireify
$ browserify -e test/spec/suite.js -t reactify -t rewireify -o test/spec/bundle.js

Rewireify very simple, it implements __get__and __set__methods in each module to their intrinsic properties can be accessed from the outside. Module dependencies can now be replaced with stubs:

/** @jsx React.DOM */
var React = require("react/addons");
var TubeTracker = require("../../../app/component/tube-tracker");
var stubComponent = require("../../lib/stub/component");
describe("Tube Tracker", function() {
  var TestUtils = React.addons.TestUtils;
  beforeEach(function() {
    this.original = {
      network: TubeTracker.__get__("Network"),
      predictions: TubeTracker.__get__("Predictions")
    };
    this.stubbed ={
      network: stubComponent(),
      predictions: stubComponent()
    };
    TubeTracker.__set__({
      Network: this.stubbed.network,
      Predictions: this.stubbed.predictions
    });
  });
  afterEach(function() {
    TubeTracker.__set__({
      Network: this.original.network,
      Predictions: this.original.predictions
    });
  });
});


Substitution of dependencies is now very simple, but components need special handling. TestUtils provides a method mockComponentthat allows you to swap the output of a passed component, but that is basically all it can do. In fact, it is sometimes more convenient to replace whole components, especially for asynchronous tests.

Jest , the Facebook wrapper recently created by the Jasmine team for Jasmine, is an alternative way to swap CommonJS dependencies. Documentation for using Jest with React is available here .

Asynchronous component testing

Not all tests can be made to run synchronously, in the case of the Tube Tracker application, the component Predictionswill always show an instance Messagebefore displaying DepartureBoard. The inability to track (spy) or replace (stub) the component’s life cycle methods, for example, componentDidMountor componentWillUnmount, is a problem since you won’t be able to know when the component will be created or destroyed.

To get around this limitation, I created a function to provide better component spoofing. The function accepts callbacks for lifecycle methods, so it becomes very convenient to insert callbacks when running tests:

/** @jsx React.DOM */
var React = require("react");
module.exports = function stub(mount, unmount) {
  var mixins = [];
  if (mount) {
    mixins.push({
      componentDidMount: function() {
        mount.call(this);
      }
    });
  }
  if (unmount) {
    mixins.push({
      componentWillUnmount: function() {
        unmount.call(this);
      }
    });
  }
  return React.createClass({
    mixins: mixins,
    render: function() {
      return 
; } }); };

Total


Testing my React applications turned out to be much more complicated than I expected. This is a new technology and we are still learning how to best use it. I had to create Rewireify and I spent a lot of time exploring the internals of React. I am not saying that all I have done is best practices, but there is not much information about how this should work. Most importantly, this works:

image

You can try the application right now (note: the example is running on a free account, so this link may be unstable) or go to GitHub to see the source code . Please comment or read to me , I will be glad to receive feedback.

Also popular now: