Writing My First Unit Tests With Jasmine And RequireJS

Posted July 6, 2012 at 3:47 PM by Ben Nadel

Tags: Javascript / DHTML

Before this morning, I had never written a Unit Test in my life. Ok, that's not 100% true - I once wrote a unit test as part of a pair-programming experiment, a few years ago, with Peter Bell. But, on my own, not once. Then, yesterday, in my blog comments, Sean Corfield finally pushed me past the tipping point. And, this morning, I downloaded Jasmine, "a behavior-driven development (BDD) framework for testing JavaScript code." And, since I am getting comfortable with RequierJS for asynchronous module loading, I figured I would try to get Jasmine and RequireJS to work together.

Since I have never used Jasmine (or any testing framework) before, I relied heavily on the Jasmine Documentation as well as a RequireJS skeleton repo by Tyler Kellen. I don't necessarily know how unit tests integrate with the app code, so ignore my architecture - I just wanted to get something working.

For this first experiment, I simply created a "test" directory in the root of my site:

  • app
  • app / js
  • app / js / lib
  • app / js / lib / jasmine-1.2.0
  • app / js / lib / require
  • app / js / model
  • app / js / model / romanNumeralEncoder.js
  • app / test
  • app / test / spec
  • app / test / spec / romanNumeralEncoder.js
  • app / test / runner.htm

In the "test" directory, the romanNumeralEncoder.js file is a "test specification", or "spec." In this context, it is a RequireJS module that defines a number of Jasmine tests to run against my model.

I know in Test-Driven Development (TDD), you're supposed to write your tests first; but, since I felt completely out of my element, I wanted to create my Model first. Of course, I didn't want to get to far into my Model, so I just created the skeleton for the RomanNumeralEncoder module:

RomanNumeralEncoder.js (Model)

  • // Define the module.
  • define(
  • [
  • /* No dependencies. */
  • ],
  • function(){
  •  
  •  
  • // I provide functionality for encoding and decoding roman
  • // numerals from a base10 radix.
  • function RomanNumeralEncoder(){
  • // ...
  • }
  •  
  • // Define the class methods.
  • RomanNumeralEncoder.prototype = {};
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Return the module constructor.
  • return( RomanNumeralEncoder );
  •  
  •  
  • }
  • );

As you can see, other than the constructor, nothing in it is defined at all.

With this in place, I then went into my "test" directory and created my runner.htm. This is the file that loads the Jasmine and RequireJS frameworks, loads the test specifications, and then tests the actual code.

runner.htm - Our Test Runner

  • <!doctype html>
  • <html>
  • <head>
  • <title>My First Unit Test With Jasmine And RequireJS</title>
  •  
  • <!-- Include the Jasmine assets for running in an HTML page. -->
  • <link rel="stylesheet" type="text/css" href="../js/lib/jasmine-1.2.0/jasmine.css"></link>
  • <script type="text/javascript" src="../js/lib/jasmine-1.2.0/jasmine.js"></script>
  • <script type="text/javascript" src="../js/lib/jasmine-1.2.0/jasmine-html.js"></script>
  •  
  • <!--
  • Since we are in the "test" directory, not in the standard
  • "js" directory, we need to define the path prefix' for the
  • RequireJS modules so that the modules can be found from the
  • tests running in the Spec directory.
  • -->
  • <script type="text/javascript">
  •  
  • var require = {
  • paths: {
  • "domReady": "../js/lib/require/domReady",
  • "model": "../js/model"
  • }
  • };
  •  
  • </script>
  • <script type="text/javascript" src="../js/lib/require/require.js"></script>
  •  
  • <!--
  • Use RequireJS to load and execute the actual Jasmine test
  • specifications (ie. specs).
  •  
  • NOTE: We have to use the domReady! plugin since the Jasmine
  • reporter needs to have access to the BODY tag of the document
  • when outputting the results of the test.
  • -->
  • <script type="text/javascript">
  •  
  • require(
  • [
  • "domReady!",
  • "spec/romanNumeralEncoder"
  • ],
  • function( document ){
  •  
  • // Set up the HTML reporter - this is reponsible for
  • // aggregating the results reported by Jasmine as the
  • // tests and suites are executed.
  • jasmine.getEnv().addReporter(
  • new jasmine.HtmlReporter()
  • );
  •  
  • // Run all the loaded test specs.
  • jasmine.getEnv().execute();
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </head>
  • <body>
  • <!-- Left intentionally blank. -->
  • </body>
  • </html>

Since the Jasmine unit tests are taking place outside of the standard "js" directory, I had to pre-define some RequieJS module paths. This way, when the test specifications (ie. specs) try to load modules in the "model" directory, RequireJS will know how to locate the relevant JavaScript files.

My test runner is only loading one test specification module, related to the RomanNumeralEncoder module I mentioned above:

RomanNumeralEncoder.js (Test Specification)

  • // Load the RomanNumeralEncoder and describe tests.
  • define(
  • [
  • "model/romanNumeralEncoder"
  • ],
  • function( RomanNumeralEncoder ){
  •  
  •  
  • // Describe the test suite for this module.
  • describe(
  • "The RomanNumeralEncoder encodes and decodes decimal values.",
  • function(){
  •  
  •  
  • // Create our test module.
  • var encoder = new RomanNumeralEncoder();
  •  
  • // Test the encoding of decimal values into roman
  • // numeral strings.
  • it(
  • "should encode decimal values",
  • function(){
  •  
  • expect( encoder.encode( 1 ) ).toBe( "I" );
  • expect( encoder.encode( 2 ) ).toBe( "II" );
  • expect( encoder.encode( 3 ) ).toBe( "III" );
  • expect( encoder.encode( 4 ) ).toBe( "IV" );
  • expect( encoder.encode( 5 ) ).toBe( "V" );
  • expect( encoder.encode( 6 ) ).toBe( "VI" );
  • expect( encoder.encode( 7 ) ).toBe( "VII" );
  • expect( encoder.encode( 8 ) ).toBe( "VIII" );
  • expect( encoder.encode( 9 ) ).toBe( "IX" );
  • expect( encoder.encode( 10 ) ).toBe( "X" );
  •  
  • }
  • );
  •  
  • // Test the decoding of roman numeral strings into
  • // decimal values.
  • it(
  • "should decode roman numeral values",
  • function(){
  •  
  • expect( encoder.decode( "I" ) ).toBe( 1 );
  • expect( encoder.decode( "II" ) ).toBe( 2 );
  • expect( encoder.decode( "III" ) ).toBe( 3 );
  • expect( encoder.decode( "IV" ) ).toBe( 4 );
  • expect( encoder.decode( "V" ) ).toBe( 5 );
  • expect( encoder.decode( "VI" ) ).toBe( 6 );
  • expect( encoder.decode( "VII" ) ).toBe( 7 );
  • expect( encoder.decode( "VIII" ) ).toBe( 8 );
  • expect( encoder.decode( "IX" ) ).toBe( 9 );
  • expect( encoder.decode( "X" ) ).toBe( 10 );
  •  
  • }
  • );
  •  
  •  
  • }
  • );
  •  
  •  
  • }
  • );

I don't yet understand how to best organize my test suites and my individual test "expectations"; so, for this first pass, I simply broke a number of encode/decode assertions into two basic tests: one for the encode() class method and one for decode() class method.

Then, in the spirit of Red-Green-Refactor, I ran my test runner to see that it would fail:


 
 
 

 
 Jasmine Unit Tests - failing. 
 
 
 

Here, the errors are indicating that our RomanNumeralEncoder() module doesn't have an encode() or a decode() method. So, I added them:

RomanNumeralEncoder.js (Model)

  • // Define the module.
  • define(
  • [
  • /* No dependencies. */
  • ],
  • function(){
  •  
  •  
  • // I provide functionality for encoding and decoding roman
  • // numerals from a base10 radix.
  • function RomanNumeralEncoder(){
  • // ...
  • }
  •  
  • // Define the class methods.
  • RomanNumeralEncoder.prototype = {
  •  
  • // I decode roman numerals into decimal values.
  • decode: function( romanNumeralValue ){
  •  
  • return( 0 );
  •  
  • },
  •  
  •  
  • // I encode decimal values as roman numerals.
  • encode: function( decimalValue ){
  •  
  • return( "" );
  •  
  • }
  •  
  • };
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Return the module constructor.
  • return( RomanNumeralEncoder );
  •  
  •  
  • }
  • );

Right now, they don't do anything logical; but, I wanted to see how their existence would change the results of the Jasmine test runner:


 
 
 

 
 Jasmine Unit Tests - failing. 
 
 
 

It's still failing; but this time, the tests fail because the expectations aren't met. Specifically, the encode() and decode() methods are not returning the expected values. So, I added some model logic to be able to superficially map roman numerals to decimal numbers:

RomanNumeralEncoder.js (Model)

  • // Define the module.
  • define(
  • [
  • /* No dependencies. */
  • ],
  • function(){
  •  
  •  
  • // I provide functionality for encoding and decoding roman
  • // numerals from a base10 radix.
  • function RomanNumeralEncoder(){
  • // ...
  • }
  •  
  • // Define the class methods.
  • RomanNumeralEncoder.prototype = {
  •  
  • // I decode roman numerals into decimal values.
  • decode: function( romanNumeralValue ){
  •  
  • // Lazy conversion, just to get something working.
  • switch ( romanNumeralValue ){
  • case "I": return( 1 );
  • case "II": return( 2 );
  • case "III": return( 3 );
  • case "IV": return( 4 );
  • case "V": return( 5 );
  • case "VI": return( 6 );
  • case "VII": return( 7 );
  • case "VIII": return( 8 );
  • case "IX": return( 9 );
  • case "X": return( 10 );
  • }
  •  
  • // If nothing matched, we don't currently support a
  • // convertion for this value.
  • return( null );
  •  
  • },
  •  
  •  
  • // I encode decimal values as roman numerals.
  • encode: function( decimalValue ){
  •  
  • // Lazy conversion, just to get something working.
  • switch ( decimalValue ){
  • case 1: return( "I" );
  • case 2: return( "II" );
  • case 3: return( "III" );
  • case 4: return( "IV" );
  • case 5: return( "V" );
  • case 6: return( "VI" );
  • case 7: return( "VII" );
  • case 8: return( "VIII" );
  • case 9: return( "IX" );
  • case 10: return( "X" );
  • }
  •  
  • // If nothing matched, we don't currently support a
  • // convertion for this value.
  • return( null );
  •  
  • }
  •  
  • };
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Return the module constructor.
  • return( RomanNumeralEncoder );
  •  
  •  
  • }
  • );

This module is obviously very limited in what it can do; but, when I run the Jasmine test runner against this module, I get a successful outcome!


 
 
 

 
 Jasmine Unit Tests - Passing!!!! 
 
 
 

Playa! Now that I have "Green" tests, I can start to refactor my code with confidence. As I replace my dumb-logic with actual business logic, I can be reassured that my application, as a whole, will remain stable... as long as my tests keep passing.

I know that this is a small first step; but it is certainly one that is long overdue!




Reader Comments

Jul 6, 2012 at 4:20 PM // reply »
13 Comments

I just kicked off a project at work incorporating TDD with Rhino Mocks, which also let's me define expectations, Webern to the point of making sure that only the expected methods run and nothing else. Really looking forward to seeing how this goes.


Jul 6, 2012 at 4:22 PM // reply »
13 Comments

That's supposed to read "even to the point...". Stupid autocorrect.


Jul 6, 2012 at 4:24 PM // reply »
11,246 Comments

@Matt,

From what I read, it looks like Jasmine can create "spies" that essentially decorate method calls a means to track whether or not they execute. Seems like a pretty robust framework.

Now, that I've tried this on the client side, I gotta try out a ColdFusion unit testing framework as well :)


Jul 6, 2012 at 4:31 PM // reply »
13 Comments

@Ben,

That's how Rhino Mocks works. It also keeps track of how many times something runs, such as in my project where I populate a Queue from the database, process the items, dequeueing them add they're handled, and them check for more records in the database. The method that retrieves records runs twice when there are results, and I can write a unit test that insures this.


Jul 6, 2012 at 5:15 PM // reply »
120 Comments

Go Ben, Go! :)

Once you get into the swing of TDD, you'll enjoy it. Writing a test lets you think about how the code needs to perform, a little piece at a time, and as you get into a rhythm of switching between test and code and test and code etc, you'll find you're producing more robust, more modular code (because testable code tends to be more modular), which in turn means it's easier to maintain and enhance the code.


Jul 6, 2012 at 8:56 PM // reply »
116 Comments

Jasmine is a very nice testing tool for JS code. I've been using it for my ExtJS apps, and write everything in CoffeeScript. You might want to check out CS, Ben.


Jul 6, 2012 at 10:17 PM // reply »
120 Comments

@Brian,

No, no!! Use ClojureScript, not CoffeeScript!! :)


Jul 6, 2012 at 11:09 PM // reply »
116 Comments

@Sean, thanks, but one thing at a time heh. Gaining the same depth of knowledge about ExtJS that I have with Flex is what I'm currently immersed in. Also, doesn't look like ClojureScript plays very nicely with ExtJS. CoffeeScript, on the other hand, really shines here since so much of ExtJS uses declarative configuration objects very similar to JSON or YAML. Still, I'm always on the lookout for more JS options, so I'll have to revisit it at some point.


Jul 7, 2012 at 11:30 PM // reply »
17 Comments

The timing of this entry could not be more perfect for me, Ben! Thank you.

I am doing exactly the same thing now. I need to build some client side data editing tool now, and I decided to introduce some unit testing into this. It is my first time with unit testing (and TDD) too (I am trying mxunit now too).

Though I have just started, it already helped me catch some bugs that I probably wouldn't be able to find without Jasmine.

By the way, the framework that I am using for this is Serenade.js. It is yet another JavaScript MVC framework and is still young (0.2.1 as of now) but it is really great! It is minimalistic but gives all the necessary functionality. The thing I love about it is that you only need to alter you model and the view will be updated, or you can alter your view and the model will be updated(2-way bindings). This framework uses its own very simple template engine, and basically what it does is giving you a DOM element with all other elements and texts nested and all necessary events already bind to it. And then you choose where to inject this element in your DOM.


Jul 8, 2012 at 1:28 PM // reply »
11,246 Comments

@Kirill,

Now that I've tried Jasmine for the client-side, I definitely gotta try something like MXUnit for the ColdFusion-side. Is that the best-in-breed for ColdFusion these days (if you know)?


Jul 8, 2012 at 4:59 PM // reply »
120 Comments

@Ben,

MXUnit is absolutely best-in-breed for CFML these days.


Jul 9, 2012 at 7:06 AM // reply »
10 Comments

Ben,
Don't forget about the ColdFusion Koans...they all use MXUnit to test CF "stuff". It might night be straight up MXUnit testing, but it might give you the basics for MXUnit, and you might learn a thing or 2 in the process :)

Some day I might be able to get into the JS testing stuff, but first trying to get MXUnit appreciated in my job...oh wait, I mean get testing unit testing appreciated in my job :)


Jul 9, 2012 at 8:58 AM // reply »
11,246 Comments

@Dan, @Sean,

Thanks, I'm gonna see if I can try MXUnit this morning. I'll also check out the Koans - I've heard great things about that, too.


Jul 9, 2012 at 3:46 PM // reply »
51 Comments

Go Ben Go!
Once you start TDD'ing the whole way you look at your code changes.

MXUnit really is best in breed for cfml.

I have not done js unit testing before myself so I was wondering what everyones opinion is on qUnit vs Jasmine?


Jul 10, 2012 at 9:41 AM // reply »
11,246 Comments

@All,

Ran my first MXUnit test:

http://www.bennadel.com/blog/2394-Writing-My-First-Unit-Tests-With-MXUnit-And-ColdFusion.htm

Getting this working was a bit more of a struggle with all the path mappings that seem to be required. Feeling empowered :D


Jul 19, 2012 at 7:53 PM // reply »
1 Comments

Awesome intro to the jasmine unit testing story for javascript developers -just wanted to drop a note and mention that you can run these tests from the command line using the jasmine-node package. (npm install jasmine-node)

We use this as our ci test runner from jenkins like so

jasmine-node tests.js


Jul 31, 2012 at 10:39 AM // reply »
11,246 Comments

@Toran,

Oh very cool! I need to completely uninstall / re-install my Node.js stuff. Well, actually, everything I've installed with Homebrew :) I accidentally installed Homebrew with "sudo" and now (I think), it requires special permissions for everything which complicates like every subsequent installation. Hmmph!


Aug 18, 2012 at 10:33 AM // reply »
1 Comments

Really? Wow.


Sep 3, 2012 at 12:26 PM // reply »
2 Comments

Great article! It helped me a lot

I'm also giving my first steps with requireJs.

Just a question, is the domReady plugin the same as enclosing the call to jasmine in $(function(){...}) ???

thanks


Sep 6, 2012 at 10:10 AM // reply »
11,246 Comments

@Opensas,

The domReady plugin is just a RequireJS plugin that "loads" when the DOM is ready to be interacted with. As you are saying (I think), this is akin to using the jQuery DOM-ready shorthand:

  • $(function(){ ... dom ready ... });

The nice thing about using the domReady plugin is that you don't have to further wrap things in your code:

  • require(
  • [ "jquery", "domReady!" ],
  • function( $ ){
  • ... DOM is ready ...
  • }
  • );

... vs:

  • require(
  • [ "jquery" ],
  • function( $ ){
  • $($function(){
  • ... DOM is ready ...
  • });
  • }
  • );

Of course, you can use either and they do the exact same thing.


Sep 7, 2012 at 2:55 AM // reply »
2 Comments

thanks to your article, and also james burke's help, I could also load jasmine itself using requireJS

have a look at this sample project: https://github.com/opensas/BackboneBootstrap/tree/master/webapp/js/src/tests


Sep 22, 2012 at 5:49 PM // reply »
11,246 Comments

@Opensas,

Very cool! Slowly, I'm really getting into the TDD stuff.


Apr 17, 2013 at 9:59 AM // reply »
2 Comments

Thanks for the write-up! It got me up and running in no time.
As a long-time ColdFusion user I'm used to your site appearing in the top 5 results for most of what I google for. It's quite amusing to see that continuing no matter what technologies I look at!


Apr 18, 2013 at 9:24 AM // reply »
11,246 Comments

@Barnaby,

Ha ha, glad I am popping up in other categories :)



Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 24, 2013 at 9:11 AM
Preventing Links In Standalone iPhone Applications From Opening In Mobile Safari
@Brandon, Hi, No, I haven't been able to do that. I have just kept it as it is. ... read »
May 23, 2013 at 9:52 PM
Preventing Links In Standalone iPhone Applications From Opening In Mobile Safari
@Muhmmadibn Did you figure out a solution to launching PDFs? I am running into the same issues myself. There is no way to close the PDF or go back once you launch it. Thanks in advance! ... read »
May 23, 2013 at 6:06 PM
The Girl Who Broke My Heart, And Made Me A Better Person
Good day,ladies and gentle men, my name is Dr AMADI the great spell caster in Africa, i have help so many people for different kind of problems,who say there is no solution to problems on earth, that ... read »
May 23, 2013 at 4:26 PM
ColdFusion QueryAppend( qOne, qTwo )
@Heather, Glad people are still getting value out of this! ... read »
May 23, 2013 at 3:49 PM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@WebManWalking, I meant the code at the bottom (not the video). I did try to experiment with an intermediary variable, like: value = users.id[ i ]; arrayContains( userIDs, value ); ... but t ... read »
May 23, 2013 at 11:06 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben, Are you talking about As Number: YES As String: YES As Java: YES? If so, that's with 3 different ways of referencing the constant 1, not users.id[1]. Query object references(*) are what seem ... read »
May 23, 2013 at 9:55 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Dan, According to the CF Admin, I'm running Java "1.6.0_45". As far as the DB column, in the database it's an INT. I'll see if I can dig into what CF sees it as. @WebManWalking, But h ... read »
May 23, 2013 at 9:49 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben, I think the problem is that we're used to loose typing in ColdFusion, like JavaScript. If a value is a number but it's needed in an expression to be a string, noooo problem. I've encountered ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools