#261 Testing JavaScript with Jasmine (revised)
- Download:
- source codeProject Files in Zip (58.5 KB)
- mp4Full Size H.264 Video (31.2 MB)
- m4vSmaller H.264 Video (15.3 MB)
- webmFull Size VP8 Video (16.4 MB)
- ogvFull Size Theora Video (37.2 MB)
With Web applications becoming increasingly complicated it’s a good idea to write automated tests for JavaScript code. Jasmine is a nice tool for doing this. It has an interface similar to RSpec’s with describe
and it
calls for defining the JavaScript’s behaviour. In this episode we’ll use Jasmine to test-drive a new JavaScript feature in a Rails application.
Test-Driving a Credit Card Form
The application we’ll be working with is an order form. The user can enter a credit card number in this form and when they do so we want to have some client-side validation to check the number using a mod 10 algorithm.
We want to be sure that this code is correct so we’ll add this feature using test-driven development with Jasmine. There are many Ruby gems available for integrating Jasmine into Rails; the one we’ll use is called Jasminerice. This has excellent support for the Rails asset pipeline and has jasmine-jquery built-in. To set it up we need to add its gem to the development and test groups in our application and then run bundle to install it.
group :development, :test do gem 'jasminerice' end
Next we’ll make a new spec/javascripts
directory in our application.
$ mkdir -p spec/javascripts
RSpec also uses the spec
directory but this completely independent of that and Jasmine doesn’t require RSpec. In this directory we’ll create a spec.js.coffee
file. This will be the central file and here we can require any other JavaScript files that we want to use in our specs. We do this using Sprockets so it works the same way as the Rails asset pipeline.
We could require the whole application’s JavaScript in this file but it can be better to pick and choose exactly what we want to require so that we can focus on what’s needed inside our specs. For now we’ll include the jquery
file and use require_tree
to include all the other specs we’ll write.
#= require jquery #= require_tree .
Our First Spec
We’ll write the specs for our credit card validation behaviour in a new file called credit_card_spec.js.coffee
. It’s not essential to use CoffeeScript but the specs look much nicer written this way than they do written in JavaScript. The first test we’ll write checks that the JavaScript code strips out any spaces and dashes from the entered number.
describe "CreditCard", -> it "strips our speces and dashes from number", -> card = new CreditCard("1 2-3") expect(card.number).toBe("123")
If our Rails app is running we can run the specs by visiting the /jasmine
path.
This shows that our one spec is failing as it can’t find the CreditCard
class which is to be expected as we haven’t yet defined it. We’ll define the class in the app/assets/javascripts
directory.
class CreditCard
We’ll need to make this class available to our specs by adding it to the spec file.
#= require jquery #= require credit_card #= require_tree .
When we reload the specs page we see the same error. This is because CoffeeScript
wraps each file within a scope so that variables defined in a file are not available globally. To remedy this we can set our CreditCard
class to a global variable.
class CreditCard @CreditCard = CreditCard
Another way to do this is to put an at sign in front of the class name, but this technique doesn’t appear to be used much so we’ll stick with what we have. When we reload our specs now we get a different error message: Expected undefined to be '123'
. This is expected as we aren’t saving the number in the class. To fix this we’ll add a constructor that takes a number and which sets an instance variable set to that number with any spaces or dashes removed.
class CreditCard constructor: (number) -> @number = number.replace(/[ -]/, '') @CreditCard = CreditCard
When we reload the specs now the error is Expected '12-3' to be '123'
. The specs have already found a bug in our code. By default JavaScript’s replace
function will only replace the first match it finds; we’ll need to alter our regular expression to replace all matches.
class CreditCard constructor: (number) -> @number = number.replace(/[ -]/g, '') @CreditCard = CreditCard
When we reload our specs page now the specs all pass.
This is a typical workflow in Jasmine. If we look at the site’s documentation we’ll see that it includes actual executable specs which show us the different matchers. All these specs are executed at the bottom of the page to show that they all pass with Jasmine.
Adding Mod 10 Validation
We still have more to do in our application so we’ll add some more specs to define our CreditCard
class. Next we’ll add a spec to test the mod 10 validation.
describe "CreditCard", -> it "strips our speces and dashes from number", -> card = new CreditCard("1 2-3") expect(card.number).toBe("123") it "validates number using mod 10", -> validCard = new CreditCard("4111-1111-1111 1111") invalidCard = new CreditCard("4111111111111112") expect(validCard.validNumber()).toBeTruthy() expect(invalidCard.validNumber()).toBeFalsy()
As expected this spec fails when we run it as we haven’t yet defined the validNumber
function that we use in the spec. We’ll add that now.
class CreditCard constructor: (number) -> @number = number.replace(/[ -]/g, '') validNumber: -> total = 0 for i in [(@number.length-1)..0] n = +@number[i] if (i+@number.length) % 2 == 0 n = if n*2 > 9 then n*2 - 9 else n*2 total += n total % 10 == 0 @CreditCard = CreditCard
When we rerun our specs now they pass again.
Even though most of the logic is implemented now we still need to present this behaviour to the user when they enter their credit card number. We can do this with some jQuery code but how can we test-drive this behaviour? The jasmine-jquery project can help here. It allows us to define HTML fixture files which we can load in to specs and provides matchers that we can use to check the jQuery behaviour. This is included with jasminerice so we don’t need to install anything else to use it. Instead we can jump right in and start creating fixture files. We’ll create a fixtures
directory under /spec/javascripts
and create an order_form.html
file in it. This file should contain some code that simulates the HTML produced by our application. We’ll add a simple form with a text field for the card number and a div
to display any errors.
<form> <input type="text" id="card_number"> <div class="error"></div> </form>
Now we can write the spec.
describe "CreditCard", -> # Other specs omitted it "validates number when field loses focus", -> loadFixtures "order_form" field = $('#card_number') field.val('123') field.blur() expect(field.next('.error')).toHaveText("Invalid card number.")
Here we load the fixture field, find the card number text box, enter an invalid credit card number into it, and then call blur()
on it to simulate the user moving on to the next field. When this happens we expect the error div
to contain the text “Invalid card number.”. We have to add one more step to this process as the jQuery code that fires when the text box isn’t loaded. It needs to be attached to the fixture that’s loaded in the spec. Normally when testing jQuery it’s easiest to make a jQuery plugin so that we can inject the functionality after the fixture has loaded. If our text field had a validateCreditCardNumber
function that jQuery functionality would be added when this code runs.
describe "CreditCard", -> # Other specs omitted it "validates number when field loses focus", -> loadFixtures "order_form" field = $('#card_number') field.validateCreditCardNumber() field.val('123') field.blur() expect(field.next('.error')).toHaveText("Invalid card number.")
When we run our specs now they fail, as expected, as that function isn’t yet defined. We’ll implement this inside the credit_card
file.
$.fn.validateCreditCardNumber = -> @each -> $(this).blur -> card = new CreditCard(@value) if !card.validNumber() $(this).next('.error').text("Invalid card number.")
This code loops through all the matching elements and listens to each one’s blur
event. When this event fires a new CreditCard
instance is created and the text field’s value is validated. If the validation fails an error message is displayed. When we run the specs again now they all pass.
We can use our new jQuery plugin to add this behaviour to our order form. All of our tests are currently passing, however, so how do we ensure that our plugin is actually working on the page? The Jasmine specs we’ve been writing so far aren’t particularly good for testing acceptance-level behaviour and we’d be better using Capybara with a JavaScript driver such as Selenium or Headless Webkit. There’s more detail on how to do this in episode 257. We’ll carry on and add this behaviour as if we had some acceptance tests in place. First we’ll modify our form and add a span element so that the error messages have a place to be displayed.
<h1>New Order</h1> <%= form_for @order do |f| %> <div class="field"> <%= f.label :card_number, "Credit Card Number" %><br /> <%= f.text_field :card_number %> <span class="error"></span> </div> <div class="field"> <%= f.label :card_expires_on, "Credit Card Expiration" %><br /> <%= f.date_select :card_expires_on, add_month_numbers: true, start_year: Time.now.year, order: [:month, :year] %> </div> <div class="actions"><%= f.submit %></div> <% end %>
In our orders.js.coffee
file we can now add the plugin’s behaviour to the credit card number field when the DOM has loaded.
jQuery -> $('#order_card_number').validateCreditCardNumber()
We can now try this our in the browser. If we reload the page and enter an invalid card number we should see an error message when we move out of that text field.
This works, but when we go back to that text field, enter a valid card number then tab out of that field again the error message remains. We have a bug in our jQuery plugin but before we fix it we’ll duplicate it in a failing spec.
describe "CreditCard", -> # Other specs omitted. it "validates number when field loses focus", -> loadFixtures "order_form" field = $('#card_number') field.validateCreditCardNumber() field.val('123') field.blur() expect(field.next('.error')).toHaveText("Invalid card number.") field.val('4111-1111-1111 1111') field.blur() expect(field.next('.error')).toHaveText("")
As expected our specs now fail again when we run them. This is easy to fix in our plugin: we’ll just add code to clear the error message when a valid credit card number has been entered.
$.fn.validateCreditCardNumber = -> @each -> $(this).blur -> card = new CreditCard(@value) if !card.validNumber() $(this).next('.error').text("Invalid card number.") else $(this).next('.error').text("")
Our specs all pass again now and if we try it in the browser it now works as expected too. There’s more we could do here such as cancelling the form submission if the number is invalid but we have a good start.
Running Tests Automatically With Guard::Jasmine
We’ll finish off this episode by showing the Guard::Jasmine gem. This gives us a great way to run our specs continuously as we work. Guard::Jasmine works headlessly and relies on Phantom.JS so we’ll need to install this first. If you’re running Mac OS X the easiest way to do this is to install it through Homebrew.
$ brew install phantomjs
In our application’s gemfile we can now add then gem then run bundle
to install it.
group :development, :test do gem 'jasminerice' gem 'guard-jasmine' end
To set up the guardfile we can run this command:
$ guard init jasmine
Now when we run guard it will monitor the spec/javascript
directory for changes.