Karma Test Runner with QUnit in 10 Minutes

This is a ten minute primer for using Karma and QUnit to unit test JavaScript.

Getting ready

Install Karma[1], plugins, and Qunit[2].

# Install Karma
npm install karma --save-dev
# Install plugins
npm install karma-qunit karma-phantomjs-launcher --save-dev
# Add global CLI
npm install -g karma-cli
# Install QUnit
npm install --save-dev qunitjs

How do it…

In your project directory, run the Karma initialization script:

karma init karma.conf.js

Answer the following questions[3]:

Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> qunit

Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> yes or no

Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> PhantomJS
>

What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> src/js/**/*.js
> test/js/**/*.js
>

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
>

Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> no

This will create the karma.conf.js configuration file for your project. To run your tests:

karma start

Put your code under the ./src/js/... directory:

src/js/core/class.js
src/js/core/input.js
src/js/core/game.js
src/js/lib/underscore-min.js
…

Put your test code under ./test/js/... directory (I mirror the src directory):

test/js/core/class.js
test/js/core/input.js
test/js/core/game.js
…

Your QUnit tests should looks something like[4]:

QUnit.test('Name of Test', function(assert) {
    // Setup the various states of the code you want to test and assert conditions.
    assert.equal(1, 1, '1 === 1');
    assert.ok(true, 'true is truthy');
    assert.ok(1, '1 is also truthy');
    assert.ok([], 'so is an empty array or object');
});

How it works…

Karma is a configurable test runner for executing any number of testing frameworks (Jasmine is really popular), but I prefer the simplicity of QUnit. It basically creates a web page, including all the files specified in the configuration, JS files followed by testing files. This runs the tests as they would be if including manually in a normal browser.

We have setup the tests to run in the PhantomJS headless browser (but you can run it against your favorite browser by updating the configuration), including all JavaScript files in the directory ./src/js/... and all testing JavaScript files in the directory ./test/js/.... Running karma start will look for a file named karma.conf.js and use it for the test runner, but you can specify another configuration with karma start other.conf.js.

Using requireJS works well with the above inclusion strategy, because the order of files doesn't matter, since each file manages its own dependencies. When not using requireJS, feel free to manually specify each file to include in karma.conf.js; they will be included in the order specified. Modifying karma.conf.js is easy and can be done any time between tests.

QUnit is a straight forward testing framework that allows you to setup conditions and assert them. Combining the two allows for quick and easy automated testing of an entire application.

When tests are setup correctly, running karma start will produce output like:

INFO [karma]: Karma v0.12.30 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.8 (Mac OS X)]: Connected on socket Cxgy5NBaHiEsOOJkawVE with id 16057191
PhantomJS 1.9.8 (Mac OS X): Executed 3 of 3 SUCCESS (0.001 secs / 0.006 secs)

Hopefully, this primer will help you get started testing your application. I have adopted Karma and QUnit for testing my simple Gaming Engine and already improved the code dramatically.

There’s more…

The errors are pretty obvious, but lets explore some common errors.

Import order issues or a file wasn’t included:

PhantomJS 1.9.8 (Mac OS X) ERROR
  ReferenceError: Can't find variable: _
  at /Volumes/UltraDisk/projects/html5-game-engine/src/js/core/asset.js:1

One of the tests fails an assertion:

INFO [karma]: Karma v0.12.30 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.8 (Mac OS X)]: Connected on socket GUH_fUDIuyUgQgO7jYE9 with id 88038210
PhantomJS 1.9.8 (Mac OS X)  Class Test FAILED
	failed, expected argument to be truthy, was: false
	Expected: true
	Actual: false
	    at /Volumes/UltraDisk/projects/html5-game-engine/node_modules/qunitjs/qunit/qunit.js:1314
	    at /Volumes/UltraDisk/projects/html5-game-engine/test/js/core/classTest.js:12
	    at /Volumes/UltraDisk/projects/html5-game-engine/node_modules/qunitjs/qunit/qunit.js:903
	    at /Volumes/UltraDisk/projects/html5-game-engine/node_modules/qunitjs/qunit/qunit.js:1032
	    at process (/Volumes/UltraDisk/projects/html5-game-engine/node_modules/qunitjs/qunit/qunit.js:591)
	    at begin (/Volumes/UltraDisk/projects/html5-game-engine/node_modules/qunitjs/qunit/qunit.js:636)
	    at /Volumes/UltraDisk/projects/html5-game-engine/node_modules/qunitjs/qunit/qunit.js:652

There is an error in karma.conf.js:

INFO [karma]: Karma v0.12.30 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
WARN [watcher]: Pattern "/Volumes/UltraDisk/projects/html5-game-engine/test/js/*^&**/*.js" does not match any file.
INFO [PhantomJS 1.9.8 (Mac OS X)]: Connected on socket 0uCl1Pg_8cbHM90Kjmfz with id 60207047

References

  1. Karma Runner
  2. QUnit
  3. Karma Configuration
  4. QUnit Assertions

Object Pool Pattern in JavaScript

Now that we understand the Recycler Object for Object Pool Pattern, we can build the logic for managing the object pool. An object pool is a simple API that manages recycling and fetching recyclable objects. There are two common models for object pools, one with a fixed number of objects that errors if too many objects are requested, and the other (more flexible approach) is to use the object pool for a fixed number ...

JavaScript Low Resolution Image Replacer

This is a handy, yet very simple, widget I was hacking on to replace loading or low resolution images with higher resolution ones once the document has finished loading.

How do it…

The widget is built using the jQuery plugin system and here is the complete code:
 (function($) { var isLoaded = false, REPLACEMENT_CLASS = "replacement-class", REPLACEMENT_RCLASS = "replacement-rclass", REPLACEMENT_URL = "replacement-img", TIMER = 500; $(window).load(function() { isLoaded = true; }); function ...

Network Information API Polyfill

One of the many new HTML5 APIs slowly being implemented by browsers is the Network Information API[2]. It exposes information about the type of network that the connecting device is using. In theory, this allows developers to optimize content around the connection speed of the user. However, as with most HTML5 APIs it is supported only by some browsers with/without prefixes, and has a legacy implementation, so a polyfill is useful when working with ...

Detecting Object Mutations by Counting Properties

Have you ever included a library and wonder, "how much did this library add to the window object", or passed an object into a function and asked yourself, "did that function modify my object"? Instead of reading the source code, this article shows a quick trick for answering these questions.

How do it…

For starters we need a function to count the number of properties on an object:
 function fnCountProperties(o) { var ...

Using Google Play Games on the Web

As many of you know, I now work for Google on the Play Games Team. We provide APIs for game developers, implementing useful features like leaderboards and achievements, so the developer doesn't have to. While many Android developers are using our services, adoption on the web could be better, so lets take a look at how to integrate the Google Play Games Services achievements into a web game.

Getting ready

Become a Google developer ...

Event Bubble & Capture Phases

One of the less understood, but powerful feature of browser events are their phases. According to the W3C level 2 spec there are three phases[1]: AT_TARGET=2, BUBBLING_PHASE=3, and CAPTURING_PHASE=1. Most browsers also implement a fourth phase[2]: NONE=0.

Getting ready

Just a quick note that everything discussed in this article is for modern browsers (all browsers except IE <9). Prior to IE 9, Internet Explorer used its own event system, instead of conforming ...

Passing Objects into addEventListener Instead of Functions

I was reviewing the browser event stack the other day and was reminded of a rarely used feature of addEventListener that allows developers to autobind the execution context object, instead of requiring a call to bind or using a library, that is worth sharing, if you weren’t already aware.

How do it…

Typically, when attaching an event, we write:
 var myObj = { handleEvent: function (evt) { // 'this' will be scoped ...

Using Promises to Cache Static AJAX JSON Data

This article showcases a useful caching strategy for static data that is fetch via AJAX. We will use jQuery to setup a promise and cache the data in the localStorage for subsequent page loads or data loads.

Getting ready

A modern web browser supporting localStorage and JSON. Also, a basic understanding of promises[2] is helpful.

How do it…

Here is the code:
 (function($) { var oKeyDeferredMap = {}; function fnReadData(sKey) ...

jQuery Function for Change Event and Delayed Keydown Event

In my experience, it is rare to assign only a change event to a text input, as any callback that would be executed for the change event should also be called on a key event as well, but with a slight delay (think how an autocomplete shows results as you type). This is a common pattern and I was surprised to not immediately find a jQuery plugin implementing it, so I decided to add one ...