Painless JavaScript testing? Surely you Jest!

title-card

Hark! What is this Jest you speak of?

Jest is an open source JavaScript testing framework built on top of Jasmine, developed by Facebook. Try saying that 10 times fast.

Think of it as several layers of improvement stuck on top of Jasmine. Some of the neat features Jest provides are:

  • Automatically finds tests to run in your project
  • Has in built support for fake DOM APIs, such as jsdom, that you can run from the command line
  • You can test asynchronous code more easily using inbuilt mocked timer functions
  • Tests are run in parallel so they go faster! Vroom vroom.

But the big drawcard is Jest’s automatic mocking of CommonJS dependencies using the require() function. Instead of specifying all the dependencies you want mocked, you do the opposite. For the subject under test, you just use jest.dontMock().

Easy peasy, right? Come with me dear reader, as I spin you a yarn… Two JavaScript test frameworks walk into a bar…

In the beginning…

Having just completed our Building Profiles project down in realcommercial.com.au (embracing the Scala Play framework) we decided to try out ReactJS on our next project: Commercial News. In writing our Jasmine/Karma tests for this project we found ourselves rewriting a lot of the same boilerplate code in our tests; mock this…mock this…mock that. Duck duck goose.

deathof-jest

Oh boy!

In React, all these components means a lot of excess code. And a lot of tests that need to pass, which can take it’s time when you’re running them for the billionith time in Karma’s browser.

In short we found that, Karma test runner was not optimal for testing our React apps.

Our distributed team in Xi’an, China decided to use Jest while working on realcommercial.com.au’s Agency Profiles project, partly because it was new and shiny, and partly because it seemed like a match made in heaven: a testing tool developed by Facebook, who also developed React. People in our Residential line of business were rumoured to also be using Jest… but we’ll discuss that a little later. It seemed to be the talk of the town. We decided to follow the rabbit down the hole for our next big project: The Mobile Site Rebuild.

THE GOOD

Jest was simple to install for us, and easy enough to understand – taking only a week of messing around to be able to use it in our app. Jest’s auto-mocking seemed to eliminate a lot of messing around with Rewire and Mockery. Testing components was the easiest thing in the world. For our email enquiry form on the project, we decided to split each section of the form into React components. This is our EnquiryMessage.js written in using React:

var React = require('react');
var $ = require('jquery');

module.exports = React.createClass({

  componentDidMount() {
    setTimeout(()=>{
      var headerHeight = $('.rui-grid.rui-header-container').height();
      var enquiryMessageOffset = $('.enquiry-message').offset().top;
      window.scroll(0, enquiryMessageOffset - headerHeight - 10); // 10 to put the message little bit under the header
    },20);
  },

  render() {
    var message = this.props.message;
    var closeCallback = this.props.closeCallback;
    var statusClass = message.status === "success" ? "rui-success" : "rui-error";
    var messageClass = `rui-message rui-message-module-fixed ${statusClass}`;

    return (
      <div className="enquiry-message">
        <div className={messageClass}>
          <i className="rui-icon rui-icon-cross close-button" onClick={closeCallback} />
          <ul>
            <li>
              <strong>{message.header}</strong>
              {message.detail}
            </li>
          </ul>
        </div>
      </div>
    );
  }
});

Aaand here is our test, written using Jest. Follow along with my comments, so you can understand what’s going on! (I’ve simplified it to one test for brevity. We’re all busy people here!)

// This is our var that lets us remove horrible ugly  ../../../ for every require
var sourceRoot = "../../../../../src/js";

// So this is the essence of Jest that lets us specify the file we DON'T want mocked
// Instead of specifying all the things we DO want mocked
jest.dontMock(sourceRoot + "/shared/components/detail/enquiry/EnquiryMessage");

// The next two lines are how we get Jasmine/Jest to play nicely with React by using React TestUtils
var React = require("react/addons");
var TestUtils = React.addons.TestUtils;

// So now we've used jest.dontMock on this file, we can now import it, and jest will not mock this data for us
// Ensuring that we're only testing this one file only. Sweet!
var EnquiryMessage = require(sourceRoot + "/shared/components/detail/enquiry/EnquiryMessage");

describe("EnquiryMessage", function() {

// We are testing here that when we close the button of the Enquiry message, that the function closeCallback is called
// Which will then change our boolean 'called' to true, which simulates the closing of the window. Pretty simple, huh?
  it("calls closeCallback when clicked on close button", function() {
    var called = false;
    var callback = function(){
      called = true;
    };

    // Using our React TestUtils library, we can use something renderIntoDocument which will make sure our EnquiryMessage component
    // shows up in our virtual DOM, and that the test knows about it!
    var enquiryMessage = TestUtils.renderIntoDocument(

      //We don't care what the actual message is for this test. All we care about, is that the closeCallback function, will call
      // the callback function. 
      <EnquiryMessage message={{}} closeCallback={callback} />
    );

    // Now we find the close-button class in our virtual DOM by using TestUtils.scryRenderedDOMComponentsWithClass
    // This lets us find the first instance of the 'close-button' class within enquiryMessage. 
    var closeButton = TestUtils.scryRenderedDOMComponentsWithClass(enquiryMessage, "close-button")[0];

    //With our TestUtils we can simulate many events without having to trigger them manually! Like, click. 
    // We click the closeButton
    //We expect 'called' to be true, which in our EnquiryMessage will close the Message!
    TestUtils.Simulate.click(closeButton);
    expect(called).toEqual(true);
  });
});

THE BAD

So Jest plays nicely with React components and the code is cleaner, and simpler to understand. Then we came to testing our server routing and metadata service called applicationMIddleware.js. This piece of code tells our service how to handle HTTP responses received from the server such as 404s or 200s. Our code looks like this:

var React = require('react');
var Router = require('react-router');
var routes = require('./../shared/routes');
var pageRenderer = require('./pageRenderer');
var handleResponses = require('./handleResponses');
var fetchData = require('../shared/fetchData');
var ErrorHandler = require('../shared/handlers/Error');

module.exports = (request, response) => {

  var router = Router.create({
    routes,
    location: request.originalUrl,
    onAbort: (result) => handleResponses({ status: 302, data: result}, request, response)
  });

  router.run( (Handler, State) => {
    fetchData(State)
      .then( data => {
        var status = (State.routes[State.routes.length-1].name === 'not-found') ? 404 : 200;
        var html = pageRenderer(Handler, State, data);

        handleResponses({ status, html }, request, response);
      }).catch( err => {
        console.log(err.stack || err);
        var data = { status: err.status ? err.status : 500 };
        var html = pageRenderer(ErrorHandler, State, data);

        handleResponses({ status: data.status, html, data: err }, request, response);
      });
  });
};

There’s a lot of requires in there!

We had our initial tests written in Jasmine,  in which we were mocking responses sent back from the server. This required a lot of setting up with the tools rewire and mockery. We had to hit a fake server and send back the response we expected, and then test what we did with that response. End to end tests are always a bit tricky. We thought we could simplify the process with Jest. But…

Every rose has its thorns

Why yes, we did run into a few problems with Jest, thank you for asking.

In fact, the longer we went using the tool, the more apparent the problems with Jest became. Such as when requiring some very common and specific libraries became a big issue, Jest would refuse to not mock them. And we would receive a lot of nondescript errors, telling us something wasn’t working. Libraries such as ‘when’, ‘winston’ and ‘path’ (amongst others) would not not mock. To solve the problem, we had to use jest.autoMockOff();to essentially turn off one of Jest’s unique features, and end up requiring the non-mocked libraries. This is not what we signed up for!

Talking to other parts of the business who ‘failed to adopt’ Jest, they would tell us that it was a pain to set up, that it would crash silently in the background without alerting anyone and when they tried to actually use it, it required an older version of NodeJS. Whaat?

Other problems we’ve encountered include the documentation being as sparse as a desert in summer and looking like something someone threw together at the last minute. Facebook’s own site for Jest has only a couple of examples and they aren’t explained very thoroughly. The upkeep of Jest on github.com is slow, but it seems to be getting there at least, with 13 merged pull requests in the last month.

Through surveys, I found out the uptake in Jest with developers seems to be really low, at around 1.05%. I’ve also heard a rumour there is just one guy at Facebook who is responsible for it. Which would seem crazy if it was a tool that FB used all the time to test their apps. Do Facebook use their own testing frameworks?

There and back again

Now we’ve had quite a rollercoaster ride with Jest. At first it was exciting and new, but the frustrations seemed to build and build. In hindsight, we probably would not use Jest again for this app. There is debate on how robust and mature it is, with some users reporting slowness or the lack of recursive ‘dontMock’ing. In the end, perhaps it’s worth trying. It’ll be sure to grow in the future, but for now it may be a little too fragile for our big projects, especially if you have a lot of end to end tests. Pick some Jasmines, and have a nice cup of Mocha instead.

happy-jest

Sources and Useful Articles

  • lizel ilano

    Fantastic blog post ! Speaking of which , if anyone require to merge some PDF files , We encountered a service here http://goo.gl/pSPkIL