Saturday, August 2, 2014

Switching from Jasmine to Mocha for Testing Polymer Elements


As a Rubyist by trade, I naturally gravitated to the testing syntax boasted by Jasmine. I still rather like it, but maybe it is time for a switch. So let's see how my tests look converted from Jasmine to Mocha

I am using this to test a relatively simple Polymer element, the very excellent <x-pizza>:



There is an SVG version of this element, but I am sticking with this bare-bones JSON version to focus more on testing. I will continue to run the tests with Karma. Also, I am using the Page Objects testing pattern here, so it will be interesting to see how easy it is to migrate my tests (all two of them) from Jasmine to Mocha.

First up, I replace the karma-jasmine dependency in my npm package.json manifest file:
{
  "name": "page_objects",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-watch": "~0.5.3",
    "karma-mocha": "~0.2.0",
    "karma-chrome-launcher": ">0.0"
  }
}
I try an npm-install, only to be greeted with:
$ npm install 
npm ERR! peerinvalid The package karma does not satisfy its siblings' peerDependencies requirements!
npm ERR! peerinvalid Peer karma-mocha@0.1.6 wants karma@>=0.12.8
npm ERR! peerinvalid Peer karma-chrome-launcher@0.1.4 wants karma@>=0.9.3
npm ERR! peerinvalid Peer karma-jasmine@0.2.2 wants karma@>=0.9
...
I agree that Karma, which is currently installed as 0.12.1 does not meet the constraints from karma-mocha:
$ npm ls karma
page_objects@ /home/chris/repos/polymer-book/book/code-js/page_objects
└── karma@0.12.1  peer invalid
But why can't npm figure out that it ought to update Karma? The outdate and update sub-commands have no effect:
$ npm outdated
Package              Current  Wanted  Latest  Location
grunt-contrib-watch    0.5.3   0.6.1   0.6.1  grunt-contrib-watch
grunt                  0.4.2   0.4.5   0.4.5  grunt
$ npm update
...
npm ERR! peerinvalid The package karma does not satisfy its siblings' peerDependencies requirements!
npm ERR! peerinvalid Peer karma-chrome-launcher@0.1.4 wants karma@>=0.9.3
npm ERR! peerinvalid Peer karma-jasmine@0.2.2 wants karma@>=0.9
npm ERR! peerinvalid Peer karma-mocha@0.1.6 wants karma@>=0.12.8
I am able to get npm to do the right thing if I completely remove the installed node_modules directory or if I manually install Karma to the latest version:
$ npm install karma
...
karma@0.12.19 node_modules/karma
...
$ npm install
$ npm ls karma karma-mocha karma-chrome-launcher
page_objects@ /home/chris/repos/polymer-book/book/code-js/page_objects
├── karma@0.12.19 
├── karma-chrome-launcher@0.1.4 
└── karma-mocha@0.1.6
I really do not understand that behavior. I doubt I will remember that, so it seems easiest to manually delete node_modules every time that I want to make a significant change to dependencies. Am I missing something?

Anyhow, a npm prune does remove karma-jasmine (as would a rm -rf node_modules), after which Karma will not start:
karma start

/home/chris/repos/polymer-book/book/code-js/page_objects/node_modules/karma/node_modules/di/lib/injector.js:9
      throw error('No provider for "' + name + '"!');
            ^
Error: No provider for "framework:jasmine"! (Resolving: framework:jasmine)
...
That was not unexpected. To resolve, I merely need to change the testing library in my karma.conf.js to use mocha:
module.exports = function(config) {
  config.set({
    // ...
    // frameworks to use
    frameworks: ['mocha'],
    // ...
  });
};
With that, I fire up Karma to find:
$ karma start
INFO [karma]: Karma v0.12.19 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 36.0.1985 (Linux)]: Connected on socket nSCPoCQz6s_jQNBp9kx1 with id 62422835
Chrome 36.0.1985 (Linux)  "before each" hook FAILED
        Error: timeout of 2000ms exceeded
            at /home/chris/repos/polymer-book/book/code-js/page_objects/node_modules/mocha/mocha.js:4288:19
Chrome 36.0.1985 (Linux): Executed 1 of 2 (1 FAILED) ERROR (2.052 secs / 2.001 secs
Ugh.

This is related to my PolymerSetup.js, which I have Karma run before anything else:
// 1. Load Polymer before any code that touches the DOM.
var script = document.createElement("script");
script.src = "/base/bower_components/platform/platform.js";
document.getElementsByTagName("head")[0].appendChild(script);

// 2. Load component(s)
var link = document.createElement("link");
link.rel = "import";
link.href = "/base/elements/x-pizza.html";
document.getElementsByTagName("head")[0].appendChild(link);

// Delay Jasmine specs until polymer-ready
var POLYMER_READY = false;
beforeEach(function(done) {
  window.addEventListener('polymer-ready', function(){
    POLYMER_READY = true;
    done();
  });
  if (POLYMER_READY) done();
});
I dynamically add the script and link tags necessary get the Polymer platform in place as well as the element being defined. That still works fine. Interestingly, both the beforeEach() call and the done() callback in the beforeEach() work the same in Mocha as they had in Jasmine. By virtue of using done as an argument to beforeEach(), the beforeEach() will block all other tests until I call done(). In this case, I will not call done() until I see the usual polymer-ready event.

The problem is not that the polymer-ready event does not fire. Rather it is a question of timing. In Jasmine, the beforeEach() is invoked immediately so that that event listener is in place before the script tags load Polymer. Mocha seems to be waiting for all scripts—even dynamically added one—to load before executing its beforeEach().

There are a couple of ways to work around this, but I opt to use Polymer's whenReady(). First I check for Polymer to be defined. Once it is, I call done() when… Polymer is ready:
var POLYMER_READY = false;
beforeEach(function(done) {
  function waitForPolymer() {
    if (Polymer) {
      Polymer.whenReady(done);
      return;
    }
    setTimeout(waitForPolymer, 1000);
  }
  waitForPolymer();

  if (POLYMER_READY) done();
});
Now that my Polymer test suite is running with Mocha, I can focus on the tests themselves. They still fail with a message about expect not being defined:
ReferenceError: expect is not defined
As an aside, seriously? To run a JavaScript test suites, I need a runner, a testing framework and an assertion library?!

I add karma-chai as another dependency:
{
  "name": "page_objects",
  "devDependencies": {
    "grunt": "*",
    "grunt-contrib-watch": "*",
    "karma-mocha": "*",
    "karma-chai": "*",
    "karma-chrome-launcher": "*"
  }
}
After a rm -rf node_modules; npm install, I update karma.conf.js to include chai as another testing framework (even though it is a assertion library):
module.exports = function(config) {
  config.set({
    // ...
    // frameworks to use
    frameworks: ['mocha', 'chai'],
    // ...
  });
}
And I get a little closer. Now I get an error about undefined not being a function:
        TypeError: undefined is not a function
            at Context. (/home/chris/repos/polymer-book/book/code-js/page_objects/test/XPizzaSpec.js:85:9)
This turns out to be slight syntax mismatch between Jasmine and Mocha Chai. Instead of testing expectations with toEqual, I need to use to.equal():
  describe('adding a whole topping', function(){
    beforeEach(function(done){
      xPizza.addWholeTopping('green peppers', done);
    });

    it('updates the pizza state accordingly', function(){
      var with_toppings = JSON.stringify({
        firstHalfToppings: [],
        secondHalfToppings: [],
        wholeToppings: ['green peppers']
      });

      expect(xPizza.currentPizzaStateDisplay()).
        to.equal(with_toppings);
    });
  });
So, in the end, not much had to change in my actual tests. The page object (the xPizza instance above) is completely unchanged. To switch from Jasmine to Mocha for my Polymer code, I had to change dependencies in npm, change the frameworks listed in the Karma configuration, change the Polymer setup code, and make a minor change to the expectation syntax. I am surprised at how similar everything else is between the two. I am unsure if I will stick with Jasmine or switch to Mocha for the book. Mocha does seem little more active (at least the Karma plugin), but I am not sure that the change would buy readers that much. Something to sleep on, I suppose.


Day #141

No comments:

Post a Comment