Writing testable JS with Jasmine

Presented by
Greg Hurrell
@causes

The state of play

  • Legacy JS code in code base
  • JS is considered "too open", because anything can be mutated
  • Overreacting, we end up hiding everything in closures, and aggressively namespacing
  • This makes testing difficult

"Hide everything" anti-pattern

  • Everything is hidden inside a closure, except for one function which is exported and called immediately on document ready
    $.namespace('Causes.Account.Privacy', (function() {
      var internalVar1;
      var internalVar2;
      function init() { /* stuff */ }
      function set_spider_visibility() { /* stuff */ }
      function set_open_graph_visibility() { /* stuff */ }
      function set_open_graph_perms_listener() { /* stuff */ }
    
      return {
        init: init
      };
    })());
    
    $(Causes.Account.Privacy.init);
  • Hard to test because it effectively makes the code a "black box"
  • Forced into using a simplistic state-based test (provide DOM, insert JS, inspect DOM after the fact)

The test-friendly way: be public by default

  • Increase the surface area of our APIs, so that Jasmine has something to interact with
  • Decompose things into simple units that can be tested independently
  • We're writing code for our own use, not publishing a framework, so we don't need an inviolable separation of "public" and "private" code
  • Optionally could use the Closure Compiler to enforce the public/private distinction; see Appendix A of Closure: The Definitive Guide (O'Reilly), also published on the author's blog

Our implementation of prototypal inheritance

  • We use Causes.classify() and subclass() as a convenient API for defining "classes"
    Causes.Paginator = Causes.classify({
      // defaults on the prototype
      targetSelector : '#item-list',
      buttonSelector : '#see-more',
    
      // functions
    
      // constructor, called automatically
      init : function(opts) {
        this.extendOptions(opts); // override defaults
      },
    
      clickHandler : function() { /* ... */ },
      loadContent: function() { /* ... */ },
      replaceContent: function() { /* ... */ }
    });
    
    Causes.FancyPaginator = Causes.Paginator.subclass({
      replaceConent: function() { /* override */ }
    });
    

Instantiating objects

  • We're currently doing this inline, near the markup that the logic relates to, as this provides nice modularity
    <div id='#sidebar-item-list' />
    <div id='#see-more' />
    <script>
      new Causes.Paginator({ targetSelector: '#sidebar-item-list' });
    </script>
  • As we're moving our JS towards the bottom of the page, this will have to change

The Jasmine DSL; RSpec-style BDD for JavaScript

describe('Causes.Paginator', function() {
  var paginator;

  beforeEach(function() { /* set-up */ });
  afterEach(function() { /* tear-down */ });

  describe('loadContent', function() {
    it('show the content div') {
      paginator.loadContent();
      expect($('#item-list').is(':visible')).toBeFalsey();
    };
});
  • It's verbose; use your editor's snippet feature to make working with the DSL less painful; see my .vim/snippets/jasmine.snippets file for an example
  • See our spec suite for examples of stubbing time, and using spies as test doubles
  • Dealing with DOM

    • Most JS is intimately tied to DOM manipulation, so we need a way of providing suitable DOM fragments to Jasmine
    • We have RSpec-side helpers for this purpose:
      describe SomeController do
        render_views
      
        describe '#show' do
          it 'saves a fixture', :jasmine => true do
            get :show
            response.should be_success # sanity check
            save_fixture(response.body, 'snippet_filename')
            save_fixture(html_for('#sidebar'), 'other_snippet')
          end
        end
      end
    • We have Rake tasks to help:
      rake jasmine                   # Run the Jasmine server
      rake jasmine:fixtures:clear    # Clears the Jasmine fixures
      rake jasmine:fixtures:generate # Generates the Jasmine fixtures
      rake jasmine:fixtures          # Clears then generates the Jasmine fixtures
      

    DOM loading on the Jasmine side

    • Fragments can be loaded like this:
      beforeEach(function() {
        // loads the fragment into #jasmine_content
        spec.loadFixture('snippet_filename');
      });
    • The #jasmine_content div is automatically cleared after each example

    Food for thought

    • As our site is so tightly integrated with Facebook, we're forced to stub a lot and rely on spies
    • Jasmine makes it easy to do unit-level tests, but higher-level integration tests are difficult
    • Much of the "testing sensibility" that you've cultivated working with Ruby/RSpec is transferrable to the world of JS
    • Always be mindful fo the cost-benefit ratio when writing tests

    Further reading

    Further reading

    Thanks