📜 ⬆️ ⬇️

Building React reliable web applications: Part 3, testing with Jasmine

Translation of the article “Building robust web apps with React: Part 3, testing with Jasmine”, by 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 process of optimizing my browser- based Tube Tracker application , but every change I make still requires updating the browser to check 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 a 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 of test , with two directories; in the lib folder, the scripts to run the tests and the spec folder, in which the tests themselves are located:

 tube tracker /
 App── app /
 Public──public /
 Test── test /
     Lib── lib /
     │ └── jasmine-2.0.0 /
     Spec── spec /
     Common ├── common /
     │ ├── component /
     │ ├── bundle.js
     Suite └── suite.js
     Spec── SpecRunner.html


In addition to the development and production environments that I described in the previous section , I added a test environment to link the application and tests together. To do this, I included all the test files (specs) in the suite.js file and 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 basic process works that way. Ease of installation also means that the tests are run in the browser, and not in a special environment, such as jsdom , which I prefer.

Note: I switched from using Bower to NPM React distribution. In the Bower version of React, utilities for testing and other add-ons come 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 with only the dependencies it needs, avoiding duplication.

React Component Testing


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

React Test Tools

To make React components easier to test, React developers provided test tools (TestUtils). Addition, which is likely to be the first thing you will find by searching for information on testing React applications. It can be used by connecting the React package with addons to the test files. The React.addons.TestUtils namespace contains methods for simulating events, sampling by components, and testing their types.

There is a very useful renderIntoDocument method that can render components into an anonymous DOM node, but for some tests, it still remains necessary to specify a container, for example, to capture events or test a 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(<ComponentAlpha title="Hello World" />); }); 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(<ComponentBeta />, 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 output components, but this does not apply to the study of their internal implementation.

Component implementation research

Representations of applications, if you work on the MVC pattern, do not contain any logic, apart from several cycles or conditions, all other logic should be rendered to the presenter. React applications do not fit this model, components may, by themselves, be small applications and some of their insides need research.

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, trying to test all the methods of the components, since, despite the fact that the methods can be invoked, you cannot modify them, at least without digging into the insides of React . Thus, the installation of stubs and mocks does not work, which at first may seem 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 inefficient when debugging errors and leads to the fact that 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 raises a problem with testing, since the dependencies of the module are not always declared public. For example, in the Tube Tracker application, the tube-tracker.js contains the network.js and predictions.js dependencies:

 /** @jsx React.DOM */ var React = require("react"); var Predictions = require("./predictions"); var Network = require("./network"); var TubeTracker = React.createClass({ render: function() { return ( <div className="layout"> <div className="layout__sidebar"> <Network networkData={this.props.networkData} /> </div> <div className="layout__content"> <Predictions line={this.state.line} station={this.state.station} networkData={this.props.networkData} /> </div> </div> ); } }); module.exports = TubeTracker; 

To circumvent 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 the basic dependency inversion pattern (IoC). Without some kind of dependency injection , using an 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 agreements, and its implementation is very different .

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

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

Rewireify is very simple; it injects __get__ and __set__ methods into each module so that their internal 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 }); }); }); 


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

Jest , the newly created Facebook team wrapper for Jasmine, is an alternative way to replace CommonJS dependencies. Documentation on 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 Predictions component will always show a Message instance before displaying the DepartureBoard . The inability to trace (spy) or replace (stub) methods of a component's life cycle, such as componentDidMount or componentWillUnmount , is a problem, since you cannot find out when a component is created or destroyed.

To circumvent this limitation, I created a function to provide better substitution of components. The function accepts callbacks for life-cycle methods, so it becomes very convenient to insert callbacks when performing 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 <div />; } }); }; 

Total


Testing my React applications was much more difficult 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 studying React's insides. I'm not saying that all I did is best practices, but there is not much information on how this should work. Most importantly, it works:

image

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

Source: https://habr.com/ru/post/229987/


All Articles