Tuesday, April 14, 2015

JavaScript Unit Testing in Crossrider Browser Extensions

Note: Please see my update on Crossrider status (12 Jan 2016).

Here's the approach I found looking for a way to implement unit tests for my JavaScript browser extension (written using Crossrider framework). The method is quite straightforward, yet still required me to do some research and testing. This approach in theory is agnostic of the testing library you use, or at least it should be. I will demonstrate it with both QUnit and Jasmine.

Setup

Let's create a basic browser extension in Crossrider, uploading the necessary library files as resources. Your folder structure may vary (“Show me your file structure and I will tell you who you are” :)), you can see how I decided to do it on the right.

Everything related to unit tests is inside “testing” sub-folder, so it doesn't mix with whatever potential resources will be added for the extension itself.

Files jasmine-tests.js and qunit-tests.js contain the testing code that I wrote—I'll show the contents of these files below.

Note that files extension.js and background.js are required by the extension (as I wrote earlier), these files contain all of the extension logic, and load additional JS resources as necessary.

Triggering Condition

Now, the key issue is that JS unit tests are usually run inside of an HTML harness—it is used to store the HTML markup necessary for the tests (if your JavaScript is presentation-heavy, which usually is the case), and also it is used to output test results.

Yet the extension doesn't run on your given HTML file, it runs in the context of each page open in the browser (speaking of the code inside extension.js file—background.js scope is a different matter, I'll talk about it below).

So here's the simple yet robust method that I've used—to stop the tests from running on every page, I check that there is a very specific string contained in the page address. For example, check that there is “test-crossrider” string at the end of the URL. Then I can trigger unit tests by opening, e.g., http://www.amaslo.com/?test-crossrider in my browser (with the extension installed).

Here's the code I've placed in extension.js:
/*
 *  extension.js
 *  Testing BDD approach with Crossrider Extensions
 */

var bRunTests = true; // Allows disabling tests in production release

appAPI.ready(function($) {
    // Unit Tests
    // Run with e.g. http://www.amaslo.com/?test-crossrider
    if(bRunTests && appAPI.isMatchPages('*test-crossrider')) {
     // QUnit
     //appAPI.resources.includeJS('testing/qunit-tests.js');
     
     // Jasmine
     appAPI.resources.includeJS('testing/jasmine-tests.js');
    }
    
    // This function will be tested
    function funcForTest(sInput) {
     return true;
    }
});

Hopefully it is self-documented enough, yet what happens is if the page address matches, and the tests are enabled—corresponding JS file is included and run.

Simple.

Actual Tests

QUnit tests:
/*
 *   Collection of Unit Tests
 *  Using QUnit
 */

(function() {
 // Testing library
 appAPI.resources.includeCSS('testing/qunit/qunit-1.18.0.css');
 appAPI.resources.includeJS('testing/qunit/qunit-1.18.0.js');
 
 // This will keep the actual page still displayed below the report
 $('body').prepend('<div id="qunit"></div><div id="qunit-fixture"></div>');
 
 QUnit.test('Set of Tests Description', function(assert) {
  var value = 'hello';
  
  assert.equal(value, 'hello', 'Expecting value to be hello');
  assert.ok(funcForTest(value), 'funcForTest() should return true');
 });
}) ();

Which results in the following output on the page:


And for Jasmine (which I frankly prefer—it feels less like robo-talk, yet this is a matter of personal preference—definitely outside of this article's scope):
/*
 *   Collection of Unit Tests
 *  Using Jasmine
 */

(function() {
 // Testing library
 appAPI.resources.includeCSS('testing/jasmine/jasmine.css');
 appAPI.resources.includeJS('testing/jasmine/jasmine.js');
 appAPI.resources.includeJS('testing/jasmine/jasmine-html.js');
 appAPI.resources.includeJS('testing/jasmine/boot.js');
 
 // Clean the document
 $('body').html('');
 
 describe('Set of Tests', function() {
  var value;
  
  beforeEach(function() {
   value = 'hello';
  });
  
  it('works', function() {
   expect(value).toBe('hello');
  });
  
  it('calls functions', function() {
   expect(funcForTest(value)).toBe(true);
  });
 });
}) ();

Yielding:

Sometimes I need to refresh the page a couple of times for Jasmine report to show up. This may be linked with extension loading / running, and doesn't really cause any inconvenience.

Both QUnit and Jasmine tests above are identical setup-wise—include library files, do whatever needs to be done with the actual page (for example just wipe it as I do for Jasmine, or append/prepend necessary markup), which will depend on what your extension does, and running the tests.

You'll notice that all unit tests are inside a closure (i.e. (function() { ... })();), this is basic variable and function scope hygiene. The functions defined in extension.js file (or the files it loads prior to running the tests) will be available in the tests' scope.

And… that's it. Really.

Real-life Tests

Of course, the above tests are extremely primitive. Checking a variable's value, and a function's return value. These only demonstrate how to incorporate unit tests in a browser extension, trigger them and view the report.

In your actual, down to Earth, real-life code you'll need to employ a lot more kung-fu from your library of choice.

For example, since my extension works with Basecamp pages, I need to have corresponding HTML markup to test the JavaScript with. Instead of depending on actual Basecamp page, which will always have changing list of todo's, I've snipped a pre-defined portion of the page and stored it in my Resources under ../testing/3rd-party/bcx.html. Then I can include this markup in the tests page (any page!) before the tests where I need it (using Jasmine):
describe('BCX Todos', function() {
 beforeEach(function() {
  var sHtml = appAPI.resources.get('testing/3rd-party/bcx.html');
  $('<div id="rm-test-bcx"></div>').appendTo('body').html(sHtml);
  // ...

Just don't forget to tear it down in afterEach() call, so that subsequent tests start from the clean slate.

For mimicking API calls you can use things like spyOn([object], [method]).and.callFake([function]), for testing asynchronous logic you'll use the “done()” callback passed into the it() statement, and so on. There's a lot to figure out, but this is part of standard unit code logic.

Testing Background and Popup Scopes

A brief note on background.js and popup.html. These contain extension code that doesn't run on a given page, so testing them could be tricky.

Depending on your situation, may I suggest minimizing the logic actually stored in background.js, and instead move everything to separate resource files (*.js). Then these files can be included and validated in unit tests.

For example, in my extension there is a lot of interaction with Roadmap API, and this is used both from extension.js and background.js. I chose to put all corresponding methods in file rm_api.js, and this file is then included as needed, in extension.js:
appAPI.resources.includeJS('js/rm_api.js');

Or in background.js (since appAPI.resources.includeJS isn't supported there):
$.globalEval(appAPI.resources.get('js/rm_api.js'));

This even avoids code repetition, so win-win. Still, I feel that this approach is not ideal. If you have better ideas—I'd love to hear them.

Regarding popup.html scope—I have no solution for now, sadly. This part of code corresponds to the popup shown on pressing the extension's button in the browser bar, and this functionality is still very limited. Neither appAPI.resources.includeJS nor appAPI.resources.get are supported in this scope, so I see no way to load additional code files into it. I guess this logic will have to be tested manually, until someone comes up with a wonderful solution.

Conclusion (and a free advice)

So, testing your extensions requires an adjustment of mindset, yet it is still possible, and not overly complicated, except for some nuances.

If you allow me to share one advice—think about unit tests before you write your code. Not after. Unless you prefer to learn the hard way. Like I did.

The reason is obvious in hindsight—structure and quality of your code is defined by necessity to call specific functions, test branches of logic, validate responses. Functions, methods, objects will almost naturally start forming into units with clearly divided responsibilities, complex spaghetti code will split into simpler pieces, and obscure solutions will look even less attractive—before you undertake to implement them!

Yes, it will take time. Yet ultimately it is a good thing. Even if tests don't “catch” all of the future issues, having a well-structured source code is a gift for the future “you”.

You'll thank yourself, trust me.

No comments:

Post a Comment