homeASCIIcasts

261: Testing JavaScript with Jasmine 

(view original Railscast)

Other translations: Ja Es

Most Rails developers understand the value of testing the Ruby code in their applications, but JavaScript is often only manually tested by running the application in a browser and checking for errors. As web applications become more complex and have more functionality on the client it would be good to have an automated way of testing JavaScript code. We’ll take a look at one way of doing just that in this episode.

The application we’ll be working with is shown below. It has a basic order form with a text field for a credit card number. We want to add some client-side validation to this field so that the number entered is validated when the field loses the focus.

The order page with the credit card field.

We’re not adding full credit card validation here but the logic is still fairly complicated so we want to make sure that this code is fully tested. We could consider using acceptance tests which were covered in episode 257 [watch, read]. These allow us to test an application at a high level by using Capybara and we can test the JavaScript in our application this way by using Selenium.

Sometimes, however, the JavaScript is complicated enough that we need something closer to the code itself, something like unit testing for JavaScript, and this is where Jasmine comes in. Jasmine is a testing framework for JavaScript that works in a smiliar way to RSpec with describe and it calls for organising tests. Of course unlike RSpec the tests are written in JavaScript, not Ruby.

Installing Jasmine

A gem is available that makes using Jasmine in Rails applications easier so we’ll start by adding it to our application and then running bundle to install it. We’ll restrict Jasmine to the development and test environments so that it isn’t used in production. Note that we’re using jQuery in this application. This isn’t required to use Jasmine but it helps.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.5'
gem 'sqlite3'
gem 'nifty-generators'
gem 'jquery-rails'
gem 'jasmine', :group => [:development, :test]

Once the gem has installed we need to run a generator to install the files that Jasmine uses.

$ rails g jasmine:install
      create  spec
      create  spec/javascripts/helpers/.gitkeep
      create  spec/javascripts/support/jasmine.yml
      create  spec/javascripts/support/jasmine_config.rb
      create  spec/javascripts/support/jasmine_runner.rb

The files are installed in a spec directory. Jasmine is generally used in conjunction with RSpec though we can use it just as well without.

Creating Our First Spec

We’re ready now to create our first JavaScript spec. Spec files belong in the /spec/javascripts folder and we’ll call this one credit_card_spec.js. (By the way if you use TextMate there’s a bundle available that provides some useful snippets such as des for creating a new description and it for creating a new it block.) Our first spec will test that the entered number is cleaned by having any dashes and spaces removed.

/spec/javascripts/credit_card_spec.js

describe("CreditCard", function() {
  it("cleans the number by removing spaces and dashes", function() {
    expect(CreditCard.cleanNumber("123 4-5")).toEqual("12345");
  });
});

Jasmine uses expect rather than the assert you might expect to check that a target has a given value. In our test we check that the cleanNumber method of a CreditCard object returns “12345” when passed “123 4-5”. Obviously, as we’re doing TDD neither the CreditCard object or the cleanNumber method exist yet.

We can run Jasmine’s spec server by running rake jasmine. This will launch a server on point 8888. If we visit that page we’ll see the results of running our specs.

Our first failing spec.

Our spec fails, as expected, and we’re going to have to write the code to get it to pass. We’ll write the code in a file called credit_card.js in the public/javascripts directory and there we’ll create the object, along with an attempt at the cleanNumber function.

/public/javascripts/credit_card.js

var CreditCard = {
  cleanNumber: function(number) {
    return number.replace(/[- ]/, "");
  }
}

When we reload the Jasmine page now we get a different error. Jasmine no longer complains about the missing CreditCard object, but it seems that cleanNumber doesn’t remove all of the spaces and dashes.

The spec still fails.

The problem lies with the regular expression which is missing a g to make it a global replace across the whole string.

/public/javascripts/credit_card.js

var CreditCard = {
  cleanNumber: function(number) {
    return number.replace(/[- ]/g, "");
  }
}

We can check that this has fixed the error by reloading the page again. When we do we’ll see that we now have a passing spec.

The spec now passes.

Next we’ll expand the validation by adding a mod 10 algorithm to perform checksum validation. We’ll write a spec with two expectations to test this code, one that expects a valid credit card number to pass and one that expects an invalid one to fail. These expectations will call a new validNumber function, one passing in a valid test Visa number that should pass and the other passing in an invalid number which should fail. Note that we have spaces and dashes in the numbers so that we can test that the validation takes these into consideration.

/spec/javascripts/credit_card_spec.js

describe("CreditCard", function () {
  it("cleans the number by removing spaces and dashes", function() {
    expect(CreditCard.cleanNumber("123 4-5")).toEqual("12345");
  });
  
  it("validates based on mod 10", function () {
    expect(CreditCard.validNumber("4111 1111 1111-1111")).toBeTruthy();
    expect(CreditCard.validNumber("4111 1111 1111-1121")).toBeFalsy();
  });
});

When we reload the Jasmine page we again see a failure, this time because it cannot find a validNumber function.

The failing spec for the mod10 validation.

We’ll write this function now, adding it to the credit_card.js file.

/public/javascripts/credit_card.js

var CreditCard = {
  cleanNumber: function(number) {
    return number.replace(/[- ]/g, "");
  },
  
  validNumber: function(number) {
    var total = 0;
    number = this.cleanNumber(number);
    for (var i=number.length-1; i >= 0; i--) {
      var n = parseInt(number[i]);
      if ((i+number.length) % 2 == 0) {
        n = n*2 > 9 ? n*2 - 9 : n*2;
      }
      total += n;
    };
    return total % 10 == 0;
  }
}

When we reload the Jasmine page now we see two passing specs.

The mod10 validation now passes.

Testing The Validation in The Browser

Now that we have some working validation we can add it to our site. For testing that the validation works on a web page we’ll use Capybara and Selenium. If the page had a lot of complex JavaScript code in it it might be difficult to test the validation code in isolation but this can be done and we’ll show you how to do that now by using HTML fixtures.

Jasmine-jquery is, as the name suggests, a jQuery extension for Jasmine. This provides fixture support, which means that we can test JavaScript against a simple HTML fragment. It also includes a long list of matchers that we can use with expect. We can install jasmine-jquery by running the following curl command, which will download the file to the spec/javascripts/helpers directory so that it’s included automatically.

$ curl http://cloud.github.com/downloads/velesin/jasmine-jquery/jasmine-jquery-1.2.0.js > spec/javascripts/helpers/jasmine_jquery-1.2.0.js

We’ll need to make a change to src_files section the jasmine.yml file in the spec/javascripts/support directory as this is configured to use Prototype by default:

/spec/javascripts/support/jasmine.yml

src_files:
  - public/javascripts/prototype.js
  - public/javascripts/effects.js
  - public/javascripts/controls.js
  - public/javascripts/dragdrop.js
  - public/javascripts/application.js
  - public/javascripts/**/*.js

We’ll replace these references with a reference to jQuery. By default every other file in the javascripts directory is included but this can lead to problems so instead we’ll include each file manually. This will also ensure that we get the load dependencies in the right order.

/spec/javascripts/support/jasmine.yml

src_files:
  - public/javascripts/jquery.js
  - public/javascripts/credit_card.js

We’re going need a directory for the fixture files so we’ll create one at /spec/javascript/fixtures. In there we’ll create a file called order_form.html that will contain a simple HTML form that will enable us to test our credit card validation. All it needs is a text field with an id so that we can reference it from JavaScript and a div to display any errors.

/spec/javascripts/fixtures/order_form.html
<form>
  <input type="text" id="card_number">
  <div id="card_number_error"></div>
</form>

To make our credit card validation code easier to use we’ll turn it into a jQuery plugin that we’ll call validateCreditCardNumber. This plugin will validate the value of any text field it’s attached to and fire when the field loses the focus. If there are any validation errors it will set the text of the element whose id is the same as the text field but with _error appended to it.

Before we create the plugin we’ll write a spec that will use our new fixture to test it.

/spec/javascripts/credit_card_spec.js

describe("CreditCard", function () {
  // Other specs omitted.
  it("validates when text field loses focus", function() {
    loadFixtures("order_form.html");
    $("#card_number").validateCreditCardNumber();
    $("#card_number").val("123");
    $("#card_number").blur();
    expect($("#card_number_error")).toHaveText("Invalid credit ↵
      card number.");
  });
});

In this spec we load the fixture by calling loadFixtures then call our new plugin on the text field. We then set the text field’s value to an invalid credit card number and call blur() on it to cause the plugin to fire. Finally we check that the element that shows the error message has the right error message in it by using the custom matcher toHaveText that jasmine-jquery provides. As expected when we reload the Jasmine page now we see a failing spec as jQuery doesn’t know about validateCreditCardNumber.

The failing spec for the jQuery plug in.

To fix this we’ll write the validateCreditCardNumber jQuery plugin. This plugin will listen to the blur event for any elements it’s attached to and will validate that element’s value when the event fires. It then uses the validNumber function we wrote earlier to check that the number is valid and, if not, will show an error.

/public/javascripts/credit_card.js

var CreditCard = {
  cleanNumber: function(number) {
    return number.replace(/[- ]/g, "");
  },
  
  validNumber: function(number) {
    var total = 0;
    number = this.cleanNumber(number);
    for (var i=number.length-1; i >= 0; i--) {
      var n = parseInt(number[i]);
      if ((i+number.length) % 2 == 0) {
        n = n*2 > 9 ? n*2 - 9 : n*2;
      }
      total += n;
    };
    return total % 10 == 0;
  }
}

(function ($){
  $.fn.validateCreditCardNumber = function () {
    return this.each(function () {
      $(this).blur(function () {
        if (!CreditCard.validNumber(this.value)) {
          $("#" + this.id + "_error").text("Invalid credit card number.");
        }
      });
    });
  };
})(jQuery);

If we reload the specs page now the spec passes and our plugin seems to work.

The plugin’s spec now passes.

Integrating the Plugin

Now that we’ve tested our plugin in isolation we’ll try to integrate it into our application. There are three files that we’ll need to change. First we’ll include our new jQuery plugin in the layout file.

/app/views/layouts/application.html.erb

<%= javascript_include_tag :defaults, "credit_card" %>

Next we’ll update the form that has the credit card field on it by adding an element to display error messages. As the form is for an Order and the field is called credit_card_number then element has to have an id of order_credit_card_number_error.

/app/views/orders/_form.html.erb

<%= form_for @order do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :credit_card_number %><br />
    <%= f.text_field :credit_card_number %>
    <span id="order_credit_card_number_error">
  </p>
  <p>
    <%= f.label :credit_card_expires_on %><br />
    <%= f.date_select :credit_card_expires_on, :add_month_numbers => true, :start_year => Time.now.year, :order => [:month, :year] %>
  </p>
  <p><%= f.submit %></p>
<% end %>

Finally we’ll add some code to application.js to add the validation to the credit card number text field.

/public/javascripts/application.js

$(function () {
  $("#order_credit_card_number").validateCreditCardNumber();
});

We can try this out now by visiting the New Order page and entering an invalid credit card number. When we tab out of the credit card number text field the error message appears.

The error message shows when an invalid credit card number is entered.

There’s a bug on the page, though. If we change the credit card number to a valid one the error message remains on the page. The message should be hidden if it’s showing when a valid number is entered.

Having found a bug the first thing we need to do is write a failing spec for it. In this case we don’t need to write a new spec, we can add an expect to the existing card validation spec that checks that the error isn’t shown for a valid number.

/spec/javascripts/credit_card_spec.js

describe("CreditCard", function () {
  // Other specs omitted.  
  it("validates when text field loses focus", function() {
    loadFixtures("order_form.html");
    $("#card_number").validateCreditCardNumber();
    $("#card_number").val("123");
    $("#card_number").blur();
    expect($("#card_number_error")).toHaveText("Invalid credit card number.");

    $("#card_number").val("4111 1111 1111-1111");
    $("#card_number").blur();
    expect($("#card_number_error")).toHaveText("");
  });
});

When we load the Jasmine page we’ll see that we’ve successfully duplicated the bug and the failing spec is shown. We expect the error div to have no text but instead it shows a message.

The failing spec to test that the error isn't shown for a valid number.

Fixing this bug is fairly simple. We can alter the code in our jQuery plugin so that the error message is hidden if the credit card number that is being validated is valid.

/public/javascripts/credit_card.js

(function ($){
  $.fn.validateCreditCardNumber = function () {
    return this.each(function () {
      $(this).blur(function () {
        if (!CreditCard.validNumber(this.value)) {
          $("#" + this.id + "_error").text("Invalid credit ↵
  card number.");
        }
        else {
          $("#" + this.id + "_error").text("");
        }
      });
    });
  };
})(jQuery);

When we reload the Jasmine page now the specs again all pass.

The spec now passes.

Finally we’ll test in in the browser again and now, when we enter an invalid number to make the error message show then enter a valid number the error disappears.

The error message is now hidden for a valid credit card number.

A Tip

Instead of reloading the page that shows the specs each time we make a change we can instead run rake jasmine:ci. When we do it will open Firefox, run the specs using Selenium and then shows the output in the terminal.

That’s it for this episode on Jasmine. Using it in your Rails applications is a great way to test the JavaScript in your application as thoroughly as the Ruby code.