Using Jasmine with Require

Last week I spoke about the basic use of Jasmine, a JavaScript testing framework. The problem presented to us by using Jasmine was that it is not currently designed to work with RequireJs. This meant we had to find a way to get them to play nicely together.

Most of the work for this I stole from Ben Nadel and Uzi Kilon but I did find I needed and wanted to make some minor changes to the code to make it work as I liked.

The fundamental problem is that require needs you to load in any used files through a call to the require function, and needs all the code to be kicked off by the main file. Require, on the other hand, expects all the files, specs and source, to be loaded in with script tags. These two approaches are not one and the same.

To get around this, first I needed to replace the main file from our actual code with some code for setting up and triggering the Jasmine tests. This Spec runner takes out the inline code from the html file and is therefore responsible for setting up any config, loading in the core libraries needed throughout the tests, loading in the tests and launching Jasmine.

Setting up config

Let’s look at this in order then, the first thing we do is load in the Require Config from our real code. Because our tests are kept a few folders away from our production code, this isn’t massively pretty but otherwise it’s the same as our regular main files:
require(['../../../main/web/js/mobile/mobileRequireConfig.js'], function(){

Then I needed to set up variables to link back to the files in the Jasmine folder since we are setting the base url to be inside the source folder. They’re a bit dirty, but they work:

    var jasmineFolder = "../../../test/javascript/jasmine/",
        specsFolder = jasmineFolder + 'mobileSpecs/';

When appended to the front of urls for files they guide us back to the jasmine folders. This means that all of the dirty business of keeping tests separate from source is dealt with here rather than impacting the production code.

Although the regular config isn’t quite perfect for us, becuase we need to replace some settings and add a few too. We do this quite simply with a call to require.config:

  require.config({
    baseUrl: "../../../main/web/js/",
    urlArgs: "cb="+Math.random(),
    paths: {
        jasmine: jasmineFolder+'lib/jasmine-1.2.0/jasmine',
        jasmineHtml: jasmineFolder+'lib/jasmine-1.2.0/jasmine-html',
        jasmineHelper: jasmineFolder+'lib/jasmine-1.2.0/jasmine-helper',
        jasmineJquery: jasmineFolder+'lib/jasmine-jquery',
        jquery: 'lib/jquery'
    },
    shim: {
      jasmine: {
        exports: 'jasmine'
      },
      jasmineHtml: ['jasmine'],
      jasmineHelper: ['jasmine'],      
      jasmineJquery: ['jasmine'],
      jquery: {
          exports: '$'
      }
    }
  });

Overwriting options like baseUrl is as easy as resetting them, because the loaded in config is processed first the config set here overrides it. However, we also need to set up some new options as well. We need to overwrite the baseUrl because it is set up relative to the html file, which is a long way from here. This is the last bit of dirtiness needed to get this all working beautifully.

urlArgs isn’t important, but by adding a random number to them we prevent files from being cached, which is just convenience. We’re also adding all the components of Jasmine to the paths and shimming it so that it can be loaded with Require.

Loading Jasmine files

Loading in the core libraries and the specs requires a nested pair of require calls. The first layer loads in the core libraries and initializes Jasmine. The second layer loads in the specs and triggers Jasmine’s execute function. Let’s take a look:

var requiredModules = [
     'jquery',
     'jasmine',
     'jasmineHtml',
     'jasmineHelper',
     'jasmineJquery'
 ];

require(requiredModules, function(){
	var jasmineEnv = jasmine.getEnv();
	jasmineEnv.updateInterval = 1000;

	var htmlReporter = new jasmine.HtmlReporter();

	jasmineEnv.addReporter(htmlReporter);

	jasmineEnv.specFilter = function(spec) {
	  return htmlReporter.specFilter(spec);
	};
	var specs = [
	    specsFolder+'myFile',
	    specsFolder+'mySpec',
	    specsFolder+'yourSpec'
	];

	require(specs, function(){
		jasmineEnv.execute();
	});

});

I have set up the list of required modules in both cases before the require call because it makes it easier to add or remove modules depending on if you’re running the tests on a server or not, which will become significant when I talk about Blanket.js. It’s not necessary, you could put those arrays straight into the require calls if you like.

Setting up a Spec

With the main file replaced I then needed each indvidual test suite have access to the modules it was testing. This involves putting a beforeEach at the top of every spec file requiring in the desired files for the test:

describe('myfile.js', function(){
	var Utils;
	beforeEach(function(){
		var flag = false;
		require(['Utils'], function(_Utils){
			Utils =  _Utils;
			flag = true;
		});

		waitsFor(function(){
			return flag;
		});

	});

...some specs...
});

Let’s pick this apart a bit. The whole test is inside a describe call. At the top of this any variables to be defined by the require call are instantiated so that all the specs have the necessary scope.

Then, inside a beforeEach we create a variable equal to false which only becomes inside the require block. This means that this will only become true once all the required modules are loaded and their variables initialized.

The waitsFor function means that this beforeEach is not considered complete until that flag becomes true, so no test will run until the modules have been loaded.

Finally, loading in the modules is done with a regular require call. You may notice that the variable being brought into the require function is _Utils when we intend to be referring to it throughout the suite as Utils. This is to avoid naming conflicts between the variable local to the require call and the variable for the whole suite.

Just to finish off let’s have a quick look at the html used to launch all this off. All it does it loads in the Jasmine CSS and favicon. Then loads require with the specRunner javascript file we just set up as its main file. This is all inside the head of the file, with a completely empty body tag.

<title>Mobile Spec Runner</title>

<link rel=”shortcut icon” type=”image/png” href=”libs/jasmine-1.2.0/jasmine_favicon.png”>
<link rel=”stylesheet” type=”text/css” href=”lib/jasmine-1.2.0/jasmine.css”>
<script type=”text/javascript” src=”../../../main/web/js/lib/require.js” data-main=”mobileSpecRunner”></script>

 

Et voila, a require-friendly suite of Jasmine tests. We’ve divided our specs into three spec runners: mobile, desktop, and shared. This is to accommodate the fact that we have a separate requireConfig for desktop and mobile. You could do more or less as you like really.

Those of you paying altogether too much attention may have noticed that this means that our main files are being untested. I couldn’t find a neat way around that problem, but I have found a way. Of sorts. Which I’ll talk about next time.

Advertisements
Tagged , , , ,

3 thoughts on “Using Jasmine with Require

  1. […] last week I spoke about testing require modules with jasmine. The problem with this approach is it can’t be used on the main files. This is for a couple […]

  2. […] I’ve Talked about Require and Jasmine and even how to get them to work together. I’ve also talked about the magical code coverage tool Blanket. Today I’m going to talk […]

  3. Bruce Harris says:

    To update your list of specs automatically as you add more tests, see this grunt plugin https://www.npmjs.org/package/grunt-amd-require-tests

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: