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
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
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
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
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
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
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 (email@example.com) % 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
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
$.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.
<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 %>
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