Engineering at Qumulo: Unit testing React UI

Posted January 12, 2016. Filed under Engineering.

Qumulo’s front-end engineering team builds UI incrementally, shipping every two weeks. As software developers, we know that changing or adding code always runs the risks of breaking something that was already working. The earlier we can catch regressions in the development cycle, the better. An example of an incrementally-delivered feature is the interactive activity graph on our main dashboard UI:

blogimage-reactui1

The first version had no presets, and we heard from customers that they wanted quickly-accessible time window selectors, so our UX team designed a preset selector. When we added a new preset React component, we had a large suite of unit tests to rely on as an early warning system to catch regressions in existing UI.

Unit testing React components

blogimage-reactui2

Here is our Jasmine unit test dashboard – all 622 tests are currently passing. For every React component we create, we always add a Jasmine test specification for it. So what do we test?

  • View logic / helper functions
  • DOM rendering (verify important elements were rendered)
  • Error-free (no React warnings or errors in the console, no exceptions thrown)

To maximize the benefits of our unit tests, we’ve adopted some conventions in our React components:

  • Use propTypes to enforce component contracts
  • Use invariant() to enforce preconditions, postconditions, and invariants

Let’s go through some code and test examples to illustrate the advantages of doing this.

Use propTypes to enforce component contracts

React optionally lets you specify the types of properties your React component accepts, and which ones are required. With hundreds of React components that are constantly changing, we’ve found it invaluable to have clearly-documented contracts for our components, with unit tests to enforce this contract. Let’s look at the example above of a 24-hour time navigator component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var Navigator = React.createClass({
   propTypes: {
       displayInfos:
           React.PropTypes.arrayOf(
               React.PropTypes.instanceOf(DisplayInfo)).isRequired,
 
       timeSeriesModel:
           React.PropTypes.instanceOf(TimeSeriesModel).isRequired,
 
       timeWindowModel:
           React.PropTypes.instanceOf(TimeWindowModel).isRequired,
   },
   // ...
});

Here we have our Navigator, which requires two data models and display info for each line to render in the graph. A simple Jasmine unit test will catch if the component contract changes by rendering the component with some test data:

beforeEach

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(function() {
   testutil.addConsoleSpies();
});
 
it("should not generate React errors in the console", function() {
    testutil.verifyNoLogOutputOnRender(
        <Navigator
            displayInfos={this.displayInfos}
            timeSeriesModel={this.timeSeriesModel}
            timeWindowModel={this.timeWindowModel}
        />
    );
});

But how do we catch propType changes in a unit test? When using the development version of React (which we use for testing), React will output warnings and errors to the console when it detects problems. Before we run tests, we add Jasmine spies to the console, and afterwards verify that nothing was written. Here’s a simplified version of our test utility:

1
2
let testutil = {
    addConsoleSpies() {

spyOn

1
(console, "warn").and.callThrough();  

spyOn

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(console, "error").and.callThrough();
    },
 
    assertNoConsoleErrors() {
        invariant(console.warn.calls.count() === 0,
            "Warnings were logged when there should be none");
        invariant(console.error.calls.count() === 0,
            "Errors were logged when there should be none");
    },
 
    verifyNoLogOutputOnRender: function(instance) {
                        //

renders the React component and calls assertNoConsoleErrors

1
2
    }
};

Now if we add a new required property to Navigator to support the new preset feature, our unit test will catch the contract change right away and we’ll remember to update all of the callers to pass in this property:

blogimage-reactui2

A side benefit: if React detects any other issues in our components, like using a deprecated feature or rendering bad HTML, it will also log those issues to the console and we’ll know right away when a test fails. Also, our tests catch React’s breaking changes when we upgrade to new versions.

You might ask: what if I forget to add the propType declaration to the React component in the first place? We catch that by using the React plugin for ESLint and enabling the prop-types rule, which ensures that all propTypes are declared.

Use invariant() to enforce preconditions, postconditions, and invariants

If you read the React source code, you’ll notice liberal use of invariant() calls to enforce preconditions, postconditions, and class invariants to alert developers when they’re using React incorrectly. React’s invariant() function is configured to throw an error in development and to no-op in production. We liked it enough that we adopted the function in our source, and write invariants for much of our code.

Precondition example:

1
2
3
updateGraph()
{ invariant(this.graph, "initializeGraph must be called first");
//

Do updates...

1
}

Instead of checking that this.graph is truthy in our updateGraph function, we could have just used the reference later in the function and let the JavaScript runtime engine throw a TypeError when trying to read a property from an undefined reference. By using a precondition at the beginning of the function, we ensure that we fail fast (and don’t allow a partial update to corrupt  our runtime state), and we also make it clear to the caller (likely a fellow developer) what they need to do to properly use this function.

Postcondition example:

1
2
groupAndResolveData() {
    let resultData;

// Do calculations...

1
2
3
4
    invariant(_.isArray(resultData) && resultData.length > 0,
        "groupAndResolveData should return an array with results");
    return resultData;
}

Here we have a helper function that does some data manipulation, with a postcondition that the result is a non-empty array. We added the invariant after discovering some cases where we would incorrectly return an object instead of an array. This check augments our unit tests when we’re doing ad-hoc testing on our UI.

Just like propTypes help enforce our React component contract, invariants help enforce our function contracts, and our unit tests quickly catch a break to these contracts.

Verifying DOM rendering

One other thing we check in our unit tests is that our React components rendered what we expect to the DOM. React components render based on two things: props and state. So our tests need to exercise interesting combinations of props and state, render, and check the output. We generally avoid doing full DOM tree comparisons because that makes our tests quite fragile as we iterate on UI. Instead, we’ll inspect the DOM and make sure certain React sub-components were rendered, or count all of the elements that have a certain CSS class.

A simple example is to check that the parent Navigator component renders the child PresetSelector component:

it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
("should render a PresetSelector", function() {
    let div = document.createElement("div");
    let navigator = ReactDOM.render(
        <Navigator
            displayInfos={this.displayInfos}
            timeSeriesModel={this.timeSeriesModel}
            timeWindowModel={this.timeWindowModel}
        />,
        div);
 
    let found =
        React.addons.TestUtils.findRenderedComponentWithType(
            navigator,
            PresetSelector);
    expect(typeof found).toBe("object");
});

Finally, we can check that the child PresetSelector component rendered four <li> entries in the DOM like so:

it

1
2
3
4
5
6
7
8
("should render four presets", function() {
    let selector = React.addons.TestUtils.renderIntoDocument(
        <PresetSelector timeWindowModel={this.timeWindowModel} />);
 
    let presets =
        React.addons.TestUtils.scryRenderedDOMComponentsWithTag(
            selector,
            "li");

expect

1
2
(presets.length).toBe(4);
});

Summary

By having strong component and function contracts (using propTypes and invariant), combined with unit tests to verify that your components are not generating React errors, you can confidently make changes to your React components knowing that regressions will be caught quickly.

Log In to Qumulo

Log In

Let's start a conversation

We are always looking for new challenges in enterprise storage. Drop us a line and we will be in touch.

Contact Information

REACH US

EMAIL

General: info@qumulo.com
PR & Media: pr@qumulo.com

WORK WITH US

SUPPORT

Search

Enter a search term below