Testing jQuery plugins with Node.js and Jasmine

I'm easing into Node.js and I wanted to share some of the stuff I'm learning. One of my focal points lately has been javascript testing. I finally got a test suite in place for my masked input plugin and I was happy with the results. I used Jasmine and the additional Jasmine-species BDD grammar. For me the next step is to be able to get a quick feedback loop on my tests without going to a browser.

So, let's start with a simple jQuery plugin.

// placeholder.js
(function($) {
	$.fn.placeholder=function(description){
		return this.each(function(){
			var input=$(this);
			input
				.bind("blur.placeholder",function(){
					input.val(input.val()||description);
				})
				.bind("focus.placeholder",function(){
					if(input.val()==description)
						input.val('');
				})
				.trigger("blur.placeholder");
		});
	};
})(jQuery);

This is a plugin that a million people have written before in various forms. This plugin provides a description of the input inside as the value of the input until it has focus. When focused, it disappears. If the user types in their own value and leaves (blur) the input, their text stays. If they type nothing, then we put the description back in.

With that knowledge, I stabbed together a few jasmine specifications in a subdirectory named "specs".

//empty.spec.js
var input=jQuery("<input />").appendTo("body").placeholder("foo");
	
describe('No value',function(){
	describe('when calling placeholder plugin', function () {
		it('should show placeholder value', function () {
			expect(input.val()).toEqual("foo");
		});
	});

	describe('when focusing input without user value', function () {
		runs(function(){input.focus();})
		it('should be empty', function () {			
			expect(input.val()).toEqual("");
		});
	});

	describe('when leaving input without user value', function () {
		runs(function(){input.focus().blur();})
		it('should show placeholder value', function () {			
			expect(input.val()).toEqual("foo");
		});
	});
});

and

//value.spec.js
var input=jQuery("<input />").appendTo("body").val("bacon").placeholder("foo");
	
describe('User supplied value',function(){
	describe('when calling placeholder plugin', function () {
		it("should have the user's value", function () {
			expect(input.val()).toEqual("bacon");
		});
	});

	describe('when focusing input with user value', function () {
		runs(function(){input.focus();})
		it("should contain the user's value", function () {			
			expect(input.val()).toEqual("bacon");
		});
	});

	describe('when leaving input with user value', function () {
		runs(function(){input.focus().blur();})
		it("should contain the user's value", function () {			
			expect(input.val()).toEqual("bacon");
		});
	});
});

Now, these may not be the well written tests, but they get the job done. If you've never seen this before, I highly recommend heading over to the jasmine website and looking through the docs.

Alright, we have a plugin and we have specifications. Let's write a Node.js script to wrap this up. First I'll post the script and then break it down afterwards.

//runspecs.js

//fake browser window
global.window = require("jsdom")
		.jsdom()
		.createWindow();
global.jQuery = require("jquery");

//Test framework
var jasmine=require('jasmine-node');
for(var key in jasmine) {
  global[key] = jasmine[key];
}

//What we're testing
require("./placeholder.js")

jasmine.executeSpecsInFolder(__dirname + '/specs', function(runner, log){  
    process.exit(runner.results().failedCount?1:0);
}, true, true);

The first thing I want to draw attention to are all of the 'require' statements in the script. These are references to node packages I've installed (except the placeholder.js one, that's the thing in the first code snippet). In order to run this script, you'll need to use the node package manager(npm) to install them on your local machine.

The first statement uses jsdom to create a mock browser window. jQuery needs this in order to do it's magic. What you'll notice though is that I'm assigning this to a property off of the global variable. By default variables you declare are only available to the script in which they are declared. By tacking this on to the global object, node will provide this window variable to all other modules or scripts we load up.

Next we load up jQuery and assign it to the global scope too. I'm doing this because my plugins reference jQuery as a global. Here's the template I've been using when writing plugins:

(function($){
   //good stuff goes here
})(jQuery)

After that, we have a few statements to load up the jasmine test framework. The for loop pulls out all of the test syntax (describe,it,runs,beforeEach,afterEach,etc) hanging off the jasmine variable and makes it a global as well.

Now things are getting good. We're finally ready to include the thing we're actually testing. This require statement will reference the path to your plugin script relative to the directory of the node script.

The final statement calls a method to invoke the jasmine specs. Notice that we're not supplying any specific file names here. Instead, we just point it to a directory which contains the specs. It does the rest. When it's done, we see if there were any failed tests and then supply an appropriate exit code.

To get this whole deal started, we simply type node runspecs.js at our terminal prompt and see how we did. When I run it, this is what I see:

$ node runspecs.js 
(node) process.compile should not be used. Use require('vm').runInThisContext instead.
Started
......

Spec No value when calling placeholder plugin
Spec No value when focusing input without user value
Spec No value when leaving input without user value
Spec No value
Spec User supplied value when calling placeholder plugin
Spec User supplied value when focusing input with user value
Spec User supplied value when leaving input with user value
Spec User supplied value
Finished in 0.003 seconds
8 tests, 6 assertions, 0 failures

Success!

  • http://aspiringcraftsman.com Derek Greer

    Good stuff! Thanks for sharing.

  • http://calvinbottoms.blogspot.com Calvin Bottoms

    Love it. Testing browser JavaScript with no browser. Quack, quack, FTW!

  • Tim Molendijk

    Testing browser JavaScript with no browser. Don’t you think that kind of misses the point of testing browser JavaScript to begin with? You are testing against V8 and jsdom. This doesn’t give you any guarantee that your code will work correctly in any other environment. Which is a problem, because we are talking about browser JavaScript here.

  • Jose Presa

    Great example, Josh! I love the portability and browser independence.

  • http://andrew-jones.com Andrew Jones

    @Tim Molendijk

    The same tests work in the browser, so of course you should test there too. But you can use node first as a “quick test”, saving a bit of time when developing.