Getting Your Javascript Under Control

js_controlDo you remember the formal announcement of the asset pipeline at RailsConf 2011? I do, I thought it was pretty cool stuff. Like almost anything in Rails, some loved it and others hated it. Either way it made waves, then in the same breath DHH announced CoffeeScript and SCSS as default. BOOOM!

Lets be honest, in 2013, nearly 2 years since the release of Rails 3.1, we no longer think about it. It has simply become part of our workflow when developing Rails applications. For no extra effort, we receive a performance boost on the client side of our applications. We are writing far better, best practice JavaScript by the virtue of CoffeeScript and no longer configuring asset packagers to concatenate and minify JavaScript and CSS. It just happens.

I also like to think it changed the way look at developing the client side of our applications. In the past year, I have probably spent more time working with JavaScript than ever before. Simply because it is no longer a pain to set up the way way I want it. I can keep it organized and, more importantly, these days it’s really easy to test.

Exponential improvements in the JavaScript engines that are plumbed into modern browsers has allowed sophisticated JS frameworks to evolve and flourish in the form of beautiful, snappy and responsive web applications.

This series is a kind of lessons learned from the frontline creating rich JS powered UI’s with Rails and focusses how to keep our client side code under control and never reverting to the “junk drawer” development days of yore.

With Great Power, yada, yada, yada

I say it flippantly, but as the client side of our applications become larger and exponentially more complex we really should be adopting the same practices we have adopted for our Ruby code.

Hands up if test drive your JavaScript?

These days I still expect to only see a few hands. I certainly didn’t test drive about a year ago. I had tests, but usually written long after the code. They were brittle and unmanageable, at best, when I did write them. Then came the day I got stung by a nasty bug amidst a mess of a jQuery and various (and not to mention numerous) associated plugins. I was well past the point of unmanageable. The most humane thing to do was put the poor thing down.

I opted to embrace some sort of framework (Backbone) and test framework (Jasmine). Even for trivial tasks I would create the Backbone view or model, drive it through tests and apply. The beauty of something like Backbone was, through it’s employ at will ethos, I could apply it to all the use cases regardless of how complex (or not) the scenario. I could also chip away one logical chunk at a time instead of an all out flame war.

I am not saying this is definitively the right way to approach your new or existing applications, but it’s certainly a right way.

Lets take a really simple, stupid scenario to illustrate. What springs to mind is one of the simple, free things that made me love rails, the flash. We want our flash to appear with a closing “X” control, if we hover over the flash it fades, clicking on the “X” it makes it disappear, it’s all very growl-like. The first thing we do is write a test.

Since we will be using Jasmine as our test framework, we’ll use the jasminerice gem. This removes the grunt of the pain we can feel dealing with the asset pipeline. To get jasminerice up and running you simply include the gem in your Gemfile

group :development, :test do
  gem "jasminerice"
end

And run the generator rails g jasminerice:install which will set up a javascripts directory within your spec directory (assuming you are using rspec) and some helper files, a dummy test and some fixtures. Fixtures are very important when testing our backbone views. You will see how we use these in a minute.

Once you have jasminerice set up in your application, delete the dummy tests that were installed and create a fixture called spec/javascripts/fixtures/flash.html with the following content

<div class="flash"><p>There is a message for the user <span class="close-alert">X</span></p></div>

This fixture is essentially the markup we will use in our application. It may differ slightly or alot but the essential ingredients are the class flash on the outer div and close-alert on the span within. Now we have enough to write a test.

Create a file spec/javascripts/flash_spec.js.coffee with the following content.

describe "Closing the alert box", ->

  beforeEach ->
    loadFixtures 'flash'

  it 'will close on a click of the cross', ->
    view = new App.Flash()
    $('.close-alert').trigger('click')
    expect(view.$el).not.toBeVisible()

In the above spec we first load the fixture into the DOM, meaning, we place it on the spec runner page for the un-initiated. Jasminerice includes the helper loadFixtures which creates a container jasmine-fixtures and appends the loaded fixtures within it. When the spec is complete, it deletes the fixture from the DOM, a rather nifty helper.

The rest of the spec sets up a new instance of the flash, triggers a click on the close element and asserts that the DOM element is no longer visible.

For those who do not have a lot of experience with Backbone, $el is the container for the view. By default it is a plain old div, but you can set it to whatever you wish. I wont dwell on it too much but just be aware the $el property you see in the above tests is a Backbone helper to pass the selector for the container element, it is equivalent to $('<your-selector') in jQuery. But as we may never know the <your-selector> of a view (dynamically set, or multiple elements on a single view in a collection) it is preferred to use $el.

Run the suite by pointing the browser at http://localhost:3000/jasmine. As expected we get a red suite with a valid enough failure. If we cheat a bit our first pass of getting green, create a file app/assets/javascripts/app.js.coffee that looks something like:

window.App = {}

App.Flash = Backbone.View.extend
  el: '.flash'

  events: {
    'click .close-alert': 'closeAlert'
  }

  closeAlert: ->
    this.$el.fadeOut()

Here we setup a namespace for the application. This is good practice in the sense we are isolating our code in the global namespace. It is still a global, but we will be restricting our usage to a known sensible namespace, App.

We then extend a Backbone view by setting the el property and binding the click event to it’s handler. You have to love the simplicity of Backbone (especially when viewed in it’s CoffeeScript form). I especially love how the events are bound within the realms of the views el so no more adding unique id’s to everything on e.currentTarget handlers.

Running the test again, bummer, we still get red. What’s going on? Is jQuery broken? Of course not, remember we are dealing with a test on an event. The jQuery default animation time on fadeOut is 400ms. Our test triggers the event, and almost immediately runs the expectation. Let’s fix that using the ignorant sleep equivalent approach:

it 'will close on a click of the cross', ->
    view = new App.Flash()
    $('.close-alert').trigger('click')
    waits 410
    runs ->
      expect(view.$el).not.toBeVisible()

it 'will close on a click of the cross', ->

Ok, the suite should now be green but we have injected a 410ms delay into it. We know that cannot be good.

Great Scott

It is time to introduce the best thing to happen to testing JavaScript sinon.js. It is chocolate to the peanut butter. Sinon is a real utility belt for testing JS. You can mock, spy, stub and call matcher assertions. Best of all, it plays very nicely with Jasmine (and all the other test frameworks as far as I’m aware). We are going to use sinon to bend time itself and remove that nasty wait/runs combo slowing up our test suite.

To include Sinon in the application simply download sinon.js into the freshly created spec/javascripts/support directory. And include the following in the spec:

  it 'will close on a click of the cross', ->
    clock = sinon.useFakeTimers();
    view = new App.Flash()
    $('.close-alert').trigger('click')
    clock.tick(410)
    expect(view.$el).not.toBeVisible()

And just like that, we have our lightning quick test suite back. Sinon really blows me away to be honest, set the clock tick to 110 and you see the spec fail as the opacity is still mid transition. Truly a beautiful piece of work in my opinion.

Next up, we want the flash to fade slightly when we hover over it. It’s a strange piece of UI at first. The notifications actually annoyed me a bit when I first started using Ubuntu, but I have become accustomed to it, especially when the notification is layered over other parts of the UI which may hold important info. In any case, at the moment we are just illustrating an example.

Testing this is very much the same as fading out the flash, only we get to use a new matcher.

it 'will fade slightly when the mouse hovers over it', ->
    clock = sinon.useFakeTimers();
    view = new App.Flash()
    view.$el.trigger('mouseenter')
    clock.tick(110)
    expect(view.$el.css('opacity')).toBe(0.1)

Using the above spec with the implementation of the following just simply won’t work.

App.Flash = Backbone.View.extend
  el: '.flash'

  events: {
    'click .close-alert': 'closeAlert',
    'mouseenter': 'mouseOver'
  }

  closeAlert: ->
    this.$el.fadeOut()

  mouseOver: ->
    this.$el.fadeTo(100, 0.1)

Basically the precision of the opacity value will be off by some tiny fraction. As such, we will use the toBeCloseTo matcher to specify the precision of the value under test, in this case 0.01 is good enough for me.

expect(view.$el.css('opacity')).toBeCloseTo(0.01, 0.1)

The fade back in is trivial. However, a thing to watch out for is, as we will be fading back up to an opacity value of 1, Jasmine will actually read the css attribute as a string. You can use good old parseInt for consistency.

expect(parseInt(view.$el.css('opacity'))).toBe(1)

Red, Green, Refactor, Refactor Tests

So far, we have written some pretty ugly specs with lots of duplication. Duplication is my favourite code smell, as it’s easy to refactor. With duplication removed we end up with a spec looking like:

describe "flash alert box", ->

  beforeEach ->
    loadFixtures 'flash'
    @view = new App.Flash()
    @clock = sinon.useFakeTimers()

  @fireEvent = (event, element, clockTick=500)->
  element.trigger(event)
  @clock.tick(clockTick)

  it 'will close on a click of the cross', ->
    @fireEvent('click', $('.close-alert'))
    expect(@view.$el).not.toBeVisible()

  it 'will fade slightly when the mouse hovers over the container', ->
    @fireEvent('mouseenter', @view.$el)
    expect(@view.$el.css('opacity')).toBeCloseTo(0.01, 0.1)

  it 'will fade back in when the mouse leaves the container', ->
    @fireEvent('mouseleave', @view.$el)
    expect(parseInt(@view.$el.css('opacity'))).toBe(1)

As you can see from the refactored test, ( just like we would refactor rspec tests) we are moving all the duplicated setup into the beforeEach function. We have also created a helper @fireEvent which takes the event, the DOM element and a clock tick. The clock tick I have made optional and defaulted to 500, as we are using sinon to fake out the clock we needn’t worry too much about it but it’s certainly worth bearing the default in mind.

Wrapping Up

Now we have a couple of tricks up our sleeves to get test driving our client side code. I would be pretty shocked if you were not thinking “that’s alot of code to write for a simple flash banner” and, to some extent, you are correct. As developers we all need to get stuff done. But why do we question tests for simple JavaScript and not things like rspec shoulda-matchers, it { should belong_to other_object }. If you are questioning it, cool. At the end of the day we have to evaluate and trade off against what value the test gives us.

I took approx 10 minutes test driving and refactoring the example here. I’ve had plenty of practice writing ‘dumb’ tests over the last year, so 10 minutes to write some specs for an element that will appear about my app at almost random intervals is acceptable.

At some point I may wish to ajax-ify the flash on a form submission, having it already within a nice Backbone structure will make that transition easier. Also, there is a bug in the demo code. It’s a small one but I know having tests in place it will be straight forward to quash (mouseenter-close-wait-mouseleave).

The next part of the series will focus some more on testing techniques and tips as well as finally getting our JavaScript under Continuous Integration (CI). Then we will round off the series catering for (DUN DUN DUN) Internet Explorer and by that I mean testing our JavaScript cross browser/platform.

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • Victor

    Thanks, awesome post. Jasminerice looks great and also sinon.js will be very helpful.