Real World Jasmine

posted on 11/01/12 at 07:06:05 pm by Joel Ross

Last time, I was just starting to play with Jasmine, so I picked something simple - the FizzBuzz kata. It worked well, but I can't remember the last time I was searching for a good FizzBuzz implementation when I was developing a web page. So I went back and found a page that I'd recently written, and looked at how I could test it.

About The Page

UnitsOfMeasureIt's a pretty simple page. The system has a few different units of measures and for each type, the user can select a default one. There's an unknown number of types and for each type, there can be any number of units of measure. For example, the page might show volume, weight, and temperature, and temperature could be set to Celsius or Fahrenheit. An example of the rendered output is on the right. The page is dynamically rendered based on the number of unit of measure types and the number of units per type.

The Initial Solution

Like I said, I wanted to write tests for a real world page, so the solution was written without any tests. It also isn't really that well organized, and there's no separation between code that performs logic and code that manipulates the DOM.

I've simplified the page quite a bit to boil it down to just the essentials, so the HTML is pretty straightforward.

   1: <html>
   2:   <head>
   3:     <title>Unit of Measure Defaults</title>
   4:     <script type="text/javascript" src="/lib/jquery.js"></script>
   5:     <script type="text/javascript" src="/src/index.js"></script>
   6:   </head>
   7:   <body>
   8:     <div>Select Unit of Measure Defaults</div>
   9:     <div id="divUnitsOfMeasure"></div>
  10:     <div><input type="submit" name="btnSave" value="Save Defaults" onclick="saveSelectedValues();" id="btnSave" /></div>
  11:     <input type="hidden" name="hdnTypes" id="hdnTypes" />
  12:   </body>
  13: </html>

This is an ASP.NET web form page, so when it's rendered, a JSON representation of the units of measure is injected into the page (variable name "uomTypes", and then serialized to hdnTypes when the user clicks the "Save Defaults" button. The JavaScript to accomplish this is below.

   1: $(document).ready(function() {
   2:     addRows();
   3: });
   5: function addRows() {
   6:     var divToAppend = $("#divUnitsOfMeasure");
   7:     for (var typeIndex in uomTypes) {
   8:         var uomType = uomTypes[typeIndex];
   9:         var id = "UoMType" + uomType.Id;
  10:         var selectBox = $("<select id=\"" + id + "\" name \"" + id + "\" />");
  11:         for (var uomIndex in uomType.UnitsOfMeasure) {
  12:             var uom = uomType.UnitsOfMeasure[uomIndex];
  13:             $("<option />", { value: uom.Id, text: uom.Name, selected: uom.IsDefault }).appendTo(selectBox);
  14:         }
  15:         var label = $("<label for=\"" + id + "\" >" + uomType.Name + ":&nbsp;</label>");
  16:         var row = $("<div />");
  17:         row.append(label);
  18:         row.append(selectBox);
  19:         divToAppend.append(row);
  20:     }
  21: }
  23: function saveSelectedValues() {
  24:     var field = $('#hdnTypes');
  25:     for (var typeIndex in uomTypes) {
  26:         var uomType = uomTypes[typeIndex];
  27:         var id = "UoMType" + uomType.Id;
  29:         var selectBox = $("#" + id);
  31:         $("#" + id + " > option").each(function(i) {
  32:             uomType.UnitsOfMeasure[i].IsDefault = this.selected;
  33:         });
  34:     }
  36:     field.val(JSON.stringify(uomTypes));
  37: };

Lots of code that accomplishes two things:

  1. On load, it creates drop downs for each unit of measure type.
  2. When saved, it serializes the units of measure to a hidden field, so it can be sent to the server and saved.

It's not the nicest JavaScript code, but it gets the job done.

Creating Tests

Now we need to write some tests for the page. First, we create a test harness for the page that we can load in the browser to run the tests. I'll leave the page out because it's very similar to the page I used for my FizzBuzz kata, with one exception. I added jasmine-jquery, a nice library that helps with creating HTML fixtures in your specs and some nice matchers for jQuery.

I wanted to test two main things:

  1. Are drop downs created for the unit of measure types?
  2. Are the units of measure stored into the hidden field when saved?

So I set out to test #1. That'd be testing the addRows() function. I quickly ran into my first issue. My functions are tightly coupled to jQuery and the DOM. Specifically, addRows() is responsible for finding the particular div that the selects will be added to. Given that I'm just testing the JavaScript and not the actual page, that div won't exist.

Remember when I said I added jasmine-jquery to my tests? This is why. With it, I can run some set up code that adds HTML that can then be used by the specs. This also lead to my first refactoring: instead of having the function find the DOM element, I'll pass it in. This makes the class less dependent on the page, and potentially reusable.

I also updated the JavaScript to use a module, instead of putting all of the methods in the global namespace, and then I added an initialize() method on the module. So that's what I'm testing with my first test:

   1: describe('When loading the view', function() {
   2:     beforeEach(function(){
   3:         jasmine.getFixtures().set("<div id='divUnitsOfMeasure'></div>");
   4:     });
   6:     it ('it should create drop downs', function() {
   7:         RossCode.UoM.addRowTo($('#divUnitsOfMeasure'));
   8:         expect($('#divUnitsOfMeasure').children().length).toBeGreaterThan(0);
   9:     });
  10: });

I could have tested for the existence of a specific dropdown based on the units of measure passed in, but for my first test, it was good enough just to ensure that something got added to the div. Notice the beforeEach() function, where it calls jasmine.getFixture().set() to stub in the div that will later be used to create the dropdowns. That's how we can get away without having a whole HTML page and just stub in enough to satisfy the method we are testing.

Luckily, the code I already had works to make this test pass, so with a few minor changes, we can get this test to pass.

   1: (function($) {
   2:     window.RossCode = window.RossCode || { };
   3:     window.RossCode.UoM = {
   4:         addRowTo: function(divToAppend) {
   5:                       for (var typeIndex in uomTypes) {
   6:                           var uomType = uomTypes[typeIndex];
   7:                           var id = "UoMType" + uomType.Id;
   8:                           var selectBox = $("<select id=\"" + id + "\" name \"" + id + "\" />");
   9:                           for (var uomIndex in uomType.UnitsOfMeasure) {
  10:                               var uom = uomType.UnitsOfMeasure[uomIndex];
  11:                               $("<option />", { 
  12:                                                   value: uom.Id, 
  13:                                                   text: uom.Name, 
  14:                                                   selected: uom.IsDefault 
  15:                                               }).appendTo(selectBox);
  16:                           }
  17:                           var label = $("<label for=\"" + id + "\" >" + uomType.Name + ":&nbsp;</label>");
  18:                           var row = $("<div />");
  19:                           row.append(label);
  20:                           row.append(selectBox);
  21:                           divToAppend.append(row);
  22:                       }
  23:                   }
  24:     }
  25: }($));

There's two main changes:

  1. Like I said, it's now a module, so there's some extra code for that.
  2. Instead of the addRows method retrieving the div itself, it's passed in.

Other than that, the code is exactly the same as before, which makes sense, since we started with working code.

Next up would be to write more tests around the population of the dropdowns - things like checking if the right number of units of measure are created, are the right defaults selected, etc. but in the interest of space, I'll leave those out.

The next thing to test is if the page saves correctly. It populates a hidden field, so the test is very similar. When the function that saves the units of measure is called, we check to see if the hidden field is populated correctly.

   1: describe('When saving the view', function() {
   2:     beforeEach(function(){
   3:         jasmine.getFixtures().set('<input type="hidden" id="hdnTypes" />');
   4:     });
   6:     it('it should save the new json to the hidden field', function() {
   7:         RossCode.UoM.saveUnitOfMeasuresJsonTo($('#hdnTypes'));
   8:         expect($('#hdnTypes').val()).toBeDefined();
   9:     });
  10: });

This is very similar to the first test, in that my refactoring involved pulling out the retrieval of the hidden field from the method itself and just passed it in. I'm also adding a fixture so I can add the HTML to the test that I need.

The code to pass this test can be added to module pretty easily:

   1: saveUnitOfMeasuresJsonTo: function(field) {
   2:     for (var typeIndex in uomTypes) {
   3:         var uomType = uomTypes[typeIndex];
   4:         var id = "UoMType" + uomType.Id;
   6:         var selectBox = $("#" + id);
   8:         $("#" + id + " > option").each(function(i) {
   9:             uomType.UnitsOfMeasure[i].IsDefault = this.selected;
  10:         });
  11:     }
  12:     field.val(JSON.stringify(uomTypes));
  13: }

Again, more tests could be written that check the script in more detail, but for simplicity, I'll leave that as an exercise  for the reader.

The Final Page

Rather than include a rehash of everything I've shown above and just putting it all together, I've pushed the code up to GitHub. My FizzBuzz specs are there, as well as my attempt at the string calculator kata.

Some Parting Thoughts on Jasmine

I've now written tests for both JavaScript that works closely with the DOM and for library-type JavaScript. I think the fact that it's extensible (like jasmine-jquery) makes it very powerful to test any type of code you want to write. It also forces you to think about whether the code you're writing has (and should have) dependencies - something I do by instinct when writing server-side code, but not so much when writing client-side code.

I really like the describe() / it() style of testing. It pretty closely follows the style of tests I'm writing to test my C# code, and I find it makes writing tests a lot quicker than writing a new test fixture or extracting out a base class.

Jasmine is definitely something I'd like to start using on a regular basis. I'm writing better code because I'm putting more thought into how it's organized than I have in the past. It's no longer Wild, Wild West coding. It's still not where I'd like to be, but I'm at least heading in the right direction.

Discuss this post

Categories: ASP.NET, Development