Back in early 2013 we published an in depth article about Continuous Integration of Javascript and Coffeescript testing with Jasmine and CasperJS. Both frameworks evolved since then. Despite its severe improvements CasperJS still remains at major version 1.0 and therefor has not introduced any significant changes how to handle its integration with TeamCity. At the same time Pivotal Labs however introduced a major version 2.0 for Jasmine in December 2013 - with some very significant changes.

This article is outlining some of the differences of Jasmine 2.0 to its predecessors when it comes to continuous integration with the TeamCity build server and how to update successfully from Jasmine 1.x to the new and shiny 2.0. For more in depth information on how to setup your continuous integration Javascript and Coffeescript testing make sure you read our previous item on the topic.

Changes in Jasmine 2.0

The clincher on updating from an earlier version to Jasmine 2.0 is mentioned here:

"Custom matchers \[...] API was barely documented and difficult to test. We've changed how matchers are added and tested." (Jasmine 2.0 release notes)

Over the past couple of months we significantly increased our javascript test coverage. On that course we introduced a little library of customised matchers. As mentioned in the release notes the handling of custom matchers was somewhat less than perfect and we had our share of struggle with it. Updating made that part a lot easier.

But as it is with every major release it brought some breaking changes. The most important one to mention here might be the complete replacement of the reporter interface.

Fear not - read on. We have you covered on that.

A bag of Gems, but which one to pick?

jasminerice

Before updating we used Brad Phelan's Jasminerice Gem to get Jasmine, Coffeescript and Rails working. Unfortunately it looks like jasminerice is abandoned by the time this article is written.

jasmine

Pivotal Labs provides the core jasmine gem. But the integration into Rails - especially for versions 3.1+ with CoffeeScript sure needs some improvements.

jasmine-rails

Luckily Justin Searls released the jasmine-rails gem which suits perfectly as a replacement for jasminerice. jasmine-rails is a simple wrapper for Pivotal's jasmine gem to use with modern Rails versions. That includes the Jasmine Core gem, what makes it particularly pleasant.

Our Pick

We decided to go with the jasmine-rails gem. It is lightweight, easy to integrate into Rails 3.1+ and contains the actual jasmine core gem, unlike jasminerice.

How to integrate Jasmine 2.0 with TeamCity

Now to the fun part. Since Pivotal™s Jasmine Gem has a distinct different architecture compared to jasminerice we need to adapt our TeamCity Reporter.

teamcity\_reporter.html

As described in our previous article the PhantomJS server used by TeamCity loads the file teamcity\_reporter.html. Within the scipt-tags we load the necessary javascript and stylesheet sources directly from the Gem.


<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
    <meta charset="utf-8" />
    <title>Console Reporter Spec

    

    
    

    
    

</head>
<body></body>
</html>

require\_jasmine.js

Jasminerice provides the jasmine library in a global javascript namespace which makes it particularly easy to call. Pivotal Labs, however, used a more object oriented approach and encapsulated the jasmine library in its own namespace. To load the library we followed the lead of the Ember Consulting Group and used RequireJS with the domReady plugin. The file *require\_jasmine.js* holds the configuration for RequireJS and Jasmine 2.0.

(function() {
  'use strict';

  require.config({
    urlArgs: "cb=" + Math.random(),
    paths: {
      'domReady': '../../script/teamcity/domReady',
      'jasmine': '../../vendor/bundle/ruby/1.9.1/gems/jasmine-core-2.0.0/lib/jasmine-core/jasmine',
      'jasmine_html': '../../vendor/bundle/ruby/1.9.1/gems/jasmine-core-2.0.0/lib/jasmine-core/jasmine-html',
      'jasmine_boot': '../../vendor/bundle/ruby/1.9.1/gems/jasmine-core-2.0.0/lib/jasmine-core/boot/boot',
      'teamcity_reporter' : '../../script/teamcity/jasmine.teamcity_reporter'
    },
    shim: {
      'jasmine': {
        exports: 'jasmine'
      },
      'jasmine_html': {
        deps: ['jasmine'],
        exports: 'jasmine'
      },
      'jasmine_boot': {
        deps: ['jasmine', 'jasmine_html'],
        exports: 'jasmine'
      },
      'teamcity_reporter': {
        deps: ['jasmine']
      }
    },
    waitSeconds: 60
  });


  require(['jasmine_boot', 'teamcity_reporter'], function () {

    require(['domReady!', '../../spec/javascripts/assets/jasmine_spec_suite'], function () {

      var TeamcityReporter = jasmineRequire.TeamcityReporter();
      window.teamcityReporter = new TeamcityReporter();
      jasmine.getEnv().addReporter(window.teamcityReporter);

      window.onload();

    });
  });

})();

Make sure you have require.js and domReady.js in your application's load path.

jasmine.teamcity\_reporter.js

Apart from name the Jasmine Reporter has changed entirely. We think for the good. It is shorter and far more lucid then it was in the first iteration.

// Setup jasmineRequire
function getJasmineRequireObj() {
  if (typeof module !== "undefined" && module.exports) {
    return exports;
  } else {
    window.jasmineRequire = window.jasmineRequire || {};
    return window.jasmineRequire;
  }
}


getJasmineRequireObj().teamcityReporter = function (jRequire, j$) {
  j$.TeamcityReporter = jRequire.TeamcityReporter();
};


getJasmineRequireObj().TeamcityReporter = function () {

  function TeamcityReporter() {

    var specCount,
        failureCount;

    this.started = false;
    this.finished = false;

    this.jasmineStarted = function () {
      this.started = true;
      specCount = 0;
      failureCount = 0;
      print("##teamcity[progressStart 'Running Jasmine Tests']");
    };


    this.jasmineDone = function () {
      this.finished = true;
      print("##teamcity[progressFinish 'Running Jasmine Tests']");
    };

    this.suiteStarted = function (suite) {
      print("##teamcity[testSuiteStarted name='" + escapeTeamcityString(suite.fullName) + "']");
    };

    this.suiteDone = function (suite) {
      print("##teamcity[testSuiteFinished name='" + escapeTeamcityString(suite.fullName) + "']");
    };

    this.specStarted = function (spec) {
      print("##teamcity[testStarted name='" + escapeTeamcityString(spec.description) + "' captureStandardOutput='true']");
    };

    this.specDone = function (result) {
      specCount++;
      if (result.status == "failed") {
        failureCount++;
        print("##teamcity[testFailed name='" + escapeTeamcityString(result.description) + "' message='" + escapeTeamcityString(result.status) + "']");
        var resultItems = result.failedExpectations;
        var outPut = "";
        for (var i = 0; i < resultItems.length; i++) {
          var resultSpec = resultItems[i];
          outPut += "\nMESSAGE:=" + escapeTeamcityString(resultSpec.message) + " MATCHER:=" + escapeTeamcityString(resultSpec.matcherName) + "  EXPECTED:=" + escapeTeamcityString(resultSpec.expected) + " ACTUAL:=" + escapeTeamcityString(resultSpec.actual) + "]";
        }
        print("##teamcity[testStdErr name='" + escapeTeamcityString(result.description) + "' out='" + outPut + "']");
      }
      print("##teamcity[testFinished name='" + escapeTeamcityString(result.description) + "']");
    };


    return this;

    function print(out) {
      console.log(out);
    }

    function escapeTeamcityString(message) {
      if (!message) {
        return "";
      }

      return message;
    }

  };

  return TeamcityReporter;
};

test\_runner.js

By that time all the necessary configuration work is done. We now need to actually run the tests. Not much changed here. We use PhantomJS to run our test suite in a headless manner. Notice that the spec suite is not longer loaded directly by the test runner script but provided through domReady and its configuration in *require\_jasmine.js*.

console.log('PhantomJS started TestRunner which loads a web page with a TeamCity Reporter...');

var page = require('webpage').create(),
    fs   = require('fs');

var currentDir = fs.workingDirectory;
var scriptDir  = currentDir + '/script/teamcity';
var reporter   = scriptDir + '/teamcity_reporter.html';

console.log("Loading " + reporter);

phantom.viewportSize = { width: 800, height: 600 };

// This is required because PhantomJS sandboxes the website and it does not show
// up the console messages form that page by default
page.onConsoleMessage = function (msg) {
    console.log(msg);

    if (msg && msg.indexOf("##jasmine.reportRunnerResults") !== -1) {
        phantom.exit();
    }
};

//Open the website
page.open(
  reporter,
  function (status) {
    //Page is loaded!
    if (status !== 'success') {
      console.log('Unable to load the address!');
    } else {
      //Using a delay to make sure the JavaScript is executed in the browser
      window.setTimeout(function () {
        // page.render("output.png");
        phantom.exit();
      }, 5000);
    }
});

Conclusion

Jasmine 2.0 adds a lot of features that make the update worth the effort. The TeamCity integration, however, was no easy update but little less than a complete rewrite of our reporter.