Plonistas! It's time to test your javascript

We're getting better and better at writing tests for Python code. Why should Javascript code be any different. We need to promise our self to test the **shit** out of our Javascript before including into Plone. So here is how I do it.

When I say "test your Javascript" I don't meant to write functional tests for your application (eg: Selenium tests), but to write proper unit tests for your Javascript code.

Problem with testing Javascript is also that there are many more environments then we are used from Python world. Just imagine that you have to test every major browser with market share greater then 5%. You quickly end up with installing every possible browser on your system and problem gets only bigger if you have to test on mobile browsers as well. So creating testing environment for Javascript is definitely not an easy task, but a tool called BusterJS can almost make this pain disappear.

Testing with BusterJS

1. Install BusterJS

We'll use npm to install BusterJS.:

% npm install buster

After that we start buster server.:

% buster server
buster-server running on http://localhost:1111

2. Capture browsers

We point any browser to the URL which is displayed once buster server is running - in our case http://localhost:1111 - and hit Capture button.

Services like BrowserStack or Browserling offer testing in browser without all the manual work needed to setup browsers and maintain them. Instead you set up tunnel to test locally on demand. I highly recomend you check them out.

3. Example tests

BusterJS also supports two ways how to write your tests, one is so called xUnit style and the other BDD style. Since BusterJS is highly pluggable you can also write your own style, like buster-qunit (runs qUnit tests) for example.

For purpose of this blog post we'll create an example test suite with few example tests to show how BusterJS tests look like.

Lets call this script example-test.js:

(function($, undefined) {
"use strict";

var testCase = buster.testCase,
    assert = buster.assert;

testCase("example case 1", {
  setUp: function() {
    $('<div id="wrapper"/>').appendTo('body');
  },
  tearDown: function() {
    $('#wrapper').remove();
  },
  // --- tests --- //
  "wrapper initialy empty": function() {
    assert($('#wrapper').html() === '');
  },
  "header tests": function() {
    setUp: function() {
      $('<div id="header">Logo</div>').appendTo('#wrapper');
    },
    tearDown: function() {
      $('#header').remove();
    },
    // --- tests --- //
    "header included in #wrapper element": function() {
      assert($('#header').parents('#wrapper').size() !== 0);
    }
  }
}

});

With tests suite above I showed how to create test module and how to nest test modules. I won't go into details since all of this is already explained in BusterJS documentation.

After test suite is created we have to register it with BusterJS. For this we create script called buster.js:

var config = module.export;
config['My first buster tests'] = {
  rootPath: "../",
  environment: "browser",
  libs: [ 'lib/jquery-1.8.2.js' ],
  sources: [ 'my-javascript-code.js' ],
  tests: [ 'test/*-test.js']
};

As you saw above in example-test.js script our script depends on jQuery. Thats why when we have to register our dependencies in buster.js.

More test examples you can find in plone.app.toolbar and don't forget to look into BusterJS documentation as well.

4. Runing tests

Our directory structure for this little example is::

resources/
  my-javascript-code.js
  lib/
    jquery-1.8.2.js
  tests/
    example-test.js

Then to run buster test we have to be in resources folder and then run::

% buster test

Thats it! You should see the result after buster test completes.

5. Bonus: Serving static files

When writing tests you usually want to introspect variables with tools like Firebug. For this BusterJS comes with possibility to start server and serve tests as static files.:

% buster static
Starting server on http://localhost:8282/

Pointing your browser to http://localhost:8282/ and off you go with debugging.

6. Bonus: Test coverage

I know coverage is not the tool to measure how good your tests are, but it's a nice tool to remind you about places in code which you forgot to test. For this we need buster extension called buster-coverage.:

% npm install buster-coverage

Then we have to adjust registration of test suite.:

var config = module.export;
config['My first buster tests'] = {
  rootPath: "../",
  environment: "browser",
  libs: [ 'lib/jquery-1.8.2.js' ],
  sources: [ 'my-javascript-code.js' ],
  tests: [ 'test/*-test.js'],
  extensions: [ require("buster-coverage") ]
};

You can also look for other buster extensions.

7. Bonus: SinonJS - spies, stubs and mocks

SinonJS, written by same author as BusterJS and also ships with it, provides Spies, stubs and mocks that can improve and simplify your tests a lot. Below I copied some examples from official SinonJS documentation.

Spies

A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. A test spy can be an anonymous function or it can wrap an existing function.

...
"calls the original function only once", function () {
  var callback = sinon.spy(),
      proxy = once(callback);

  proxy();

  assert(callback.called);
},
...

Stubs

Test stubs are functions (spies) with pre-programmed behavior. Use a stub when you want to:

  1. Control a method's behavior from a test to force the code down a specific path. Examples include forcing a method to throw an error in order to test error handling.
  2. When you want to prevent a specific method from being called directly (possibly because it triggers undesired behavior, such as a XMLHttpRequest or similar).
"test should stub method differently based on arguments": function () {
  var callback = sinon.stub();
  callback.withArgs(42).returns(1);
  callback.withArgs(1).throws("TypeError");

  callback(); // No return value, no exception
  callback(42); // Returns 1
  callback(1); // Throws TypeError
}

Mocks

Mocks (and mock expectations) are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations. A mock will fail your test if it is not used as expected.

Mocks come with built-in expectations that may fail your test. Thus, they enforce implementation details. The rule of thumb is: if you wouldn't add an assertion for some call specific, don't mock it. Use a stub instead. In general you should never have more than one mock (possibly with several expectations) in a single test.:

"test should call all subscribers when exceptions": function () {
  var myAPI = { method: function () {} };

  var spy = sinon.spy();
  var mock = sinon.mock(myAPI);
  mock.expects("method").once().throws();

  PubSub.subscribe("message", myAPI.method);
  PubSub.subscribe("message", spy);
  PubSub.publishSync("message", undefined);

  mock.verify();
  assert(spy.calledOnce);
}

8. Integration with buildout

During Sea Sprint 2012 Ross Patterson got a nice idea how to run buster tests in a Plone friendly way. He created buildout recipe which creates a script, that starts buster server, uses Selenium to Capture browsers and runs busters tests. Here is what you need to add to your buildout in order to get buster tests running.:

[buildout]
parts +=
    jenkins-buster-firefox-test

[jenkins-buster-firefox-test]
recipe = buster-selenium[recipe]
eggs = my.package[test]
defaults = ['--layer=BusterJSSlaveLayer', '--tests-pattern=^test?']
environment = jenkins-buster-firefox-env

[jenkins-buster-firefox-env]
BUSTER_SERVER_EXECUTABLE = ${buildout:directory}/node_modules/.bin/buster-server
BUSTER_TEST_EXECUTABLE = ${buildout:directory}/node_modules/.bin/buster-test
BUSTER_SLAVE_SELENIUM_DRIVER = Firefox
BUSTER_TEST_OPTIONS = --reporter xml
BUSTER_TEST_STDOUT = ${buildout:directory}/parts/jenkins-test/buster-test.xml

Probably buster-selenium was not yet released, but you can go and add it to your buildout with mr.developer. Then simply run bin/jenkins-buster-firefox-test script.

Conclusion

As you see testing in Javascript is easy, as it is in Python and there is no reason why your Javascript shouldn't be tested.

comments powered by Disqus