Jasmine Testing

Backbone.js Models and Views

http://jasmine-backbone.herokuapp.com

Brenda Jin @cyberneticlove

Overview

  • Brief overview of testing
  • Testing Models
  • Testing Views

Why test?

Code is for humans

(and so are the bugs)

  • set expectations
  • think through the steps
  • break down the pieces
  • encourage modularity and stability
  • ensure ease of upgrades and deprecations

Jasmine

describe, it, and expect

Suites and specs are written with strings in plain English that describe exactly what is going on to the human developer user

example

In your spec file

describe("Tests for a custom Backbone Model", function() {
  var macys;

  it("can have a custom method called calculateAge", function() {
    macys = new StoreModel();
    expect(macys.calculateAge).toBeDefined();
  });
});

In your src file

var StoreModel = Backbone.Model.extend({
  calculateAge: function() {
    ...
  }
});

Jasmine

beforeEach and afterEach

  • Set the stage
  • Let the players play their parts
  • Tear down the stage

example

describe("Tests for a custom Backbone Model", function() {
  var macys;

  beforeEach(function() {
    macys = new StoreModel({
      yearFounded: 1858
    });
  });

  it("can have a custom method called calculateAge", function() {
    expect(macys.calculateAge).toBeDefined();
  });
});

If you would like to follow along...

Download the files

Testing Models

Sample Spec

Building blocks

  1. set up and instantiate
  2. defaults and methods
  3. initialization process
  4. control flow
  5. events and callbacks

Set Up and Instantiate

Define your variable in a scope that makes sense for your specs, then either instantiate per test or in a beforeEach() block

describe("Tests for a custom Backbone Model", function() {
  var macys;

  beforeEach(function() {
    macys = new StoreModel({
      brandName: 'macys',
      website: 'http://www.macys.com',
      yearFounded: 1858,
      isDepartmentStore: true
    });
  })
  ...

Defaults

Make sure expected defaults exist

it("can have default properties such as brandName, website, yearFounded, 
and isDepartmentStore", function() {
  expect(macys.defaults).toBeDefined();
  expect(macys.defaults.brandName).toBeDefined();
  expect(macys.defaults.website).toBeDefined();
  expect(macys.defaults.yearFounded).toBeDefined();
  expect(macys.defaults.isDepartmentStore).toBeDefined();
});

Tip: when you are setting defaults, think about how the absence of a property will be rendered in a template.

Methods

Make sure they exist

it("can have a custom method called calculateAge", function() {
  expect(macys.calculateAge).toBeDefined();
});

And make sure they do the right thing

it("will set the age after calculateAge is called", function() {
  macys.calculateAge();
  expect(macys.get('age')).toBeDefined();
  expect(macys.get('age')).toEqual((new Date().getFullYear())
   - macys.get('yearFounded'));
});

Initialization Process

Use spies to make sure the right initialization steps are taken

it("can check to see if models are instantiated correctly", function() {
  spyOn(macys, 'calculateAge');
  macys.initialize();
  expect(macys.calculateAge).toHaveBeenCalled();
});

Control Flow

Similar to initialization, you can use spies to test control flow

it("can make sure that fetchDepartments is called with the 
when instantiated, but only if isDepartmentStore
is true", function() {
  spyOn(macys, 'fetchDepartments');

  // remember isDepartmentStore was set to true
  macys.initialize();
  expect(macys.fetchDepartments).toHaveBeenCalled();

  macys.set('isDepartmentStore', false);
  macys.initialize();

  expect(macys.fetchDepartments.calls.count()).toEqual(1);
});

Events and Callbacks

Make sure that custom or built-in events are triggered correctly using spies for event listeners in the spec

it("can test Model events using spies in the spec", function() {
  var myObject = {
    aFakeCallback: function() {
      return true;
    }
  }

  spyOn(myObject, 'aFakeCallback');
  macys.on('customEvent', myObject.aFakeCallback);

  macys.triggerCustomEvent();
  expect(myObject.aFakeCallback).toHaveBeenCalled();
});

Testing Views

Sample Spec

Building blocks

  1. set up and instantiate
  2. DOM events and their callbacks
  3. interaction with model/collection
  4. functional DOM changes

(In addition to similarities with models like custom methods, initialization, control flow, and custom events)

Set Up and Instantiate

describe("Tests for a custom Backbone Model", function() {
  var macys, macysView;

  beforeEach(function() {
		macys = new StoreModel({
      brandName: 'macys',
      website: 'http://www.macys.com',
      yearFounded: 1858,
      isDepartmentStore: true
    });

    macysView = new StoreView({
      model: macys
    });
  });
  ...

DOM events and callbacks

Use object bracket notation to check events and their callbacks

it("can test for default events like a ul click", function() {
 expect(macysView.events['click ul li']).toBeDefined();
 expect(macysView.events['click ul li']).toEqual('clickCallback');
});

Then, test the functionality of each callback

Interaction with a model/collection

You can test a View's reaction to data changes

// in beforeEach
spyOn(StoreView.prototype, 'render').and.callThrough();
// in your spec
it("can test for reactions to Model-level events", function() {
  macys.set('isDepartmentStore', false);
  expect(macysView.render).toHaveBeenCalled();
});

Functional DOM changes

it("can test for functional DOM changes", function() {
  var oldLength, newLength; 

  $('body').append(macysView.render().$el);
  
  oldLength = macysView.$('li').length;

  macysView.$('ul li').click();
  
  newLength = macysView.$('li').length;

  expect(newLength - oldLength).toEqual(1);

  macysView.remove();
});