Skip to content

Instantly share code, notes, and snippets.

@ngpestelos
Created October 19, 2012 10:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ngpestelos/3917443 to your computer and use it in GitHub Desktop.
Save ngpestelos/3917443 to your computer and use it in GitHub Desktop.
Outline for Jasmine + Scenar.io talk at Aelogica (October 18, 2012)

3 Common Tasks Using Jasmine.js

We want to share what we've learned using Jasmine to test Scenar.io's JavaScript code. Scenar.io is a collaborative story carding app built on backbone.js, CoffeeScript, Rails 3.2, and MongoDB. Also, I felt that I was unable to give a proper retrospective while I was actively working on the project.

We used Jasmine to write specs for Scenar.io's backbone.js objects (e.g. StoryView, StoryModel, ProjectView, ProjectModel, StoriesCollection, etc.). Here are some of our common tasks:

1. Test backbone.js Views and Models

View: Trigger a jQuery Event and verify the view properties (using its $el property) has changed.

// story_view_spec.js
describe "mouse move", ->
  beforeEach ->
    @view.selected = true
    e = jQuery.Event("mousemove", { target: @view.el })
    @view.el.trigger(e)
  it "should indicate that it is moving", ->
    expect (@view.el.hasClass("move")).toBeTruthy()
  it "should indicate that it is unselectable", ->
    expect (@view.el.hasClass("unselectable")).toBeTruthy()

Model: Verify that a collaborator has been added to the project.

describe "A project with an owner and a collaborator", ->
   beforeEach ->
      @owner = new Scenario.Models.User({_id: 400})
      @collaborator = new Scenario.Models.User({_id: 500})
      @project = new Scenario.Models.Project()
      @project.set("owner_id", @owner.id)
      @project.users().add(@owner)
      @project.users().add(@collaborator)

    it "should have an owner", ->
      expect(@project.owner()).toEqual(@owner)

    it "should have 2 users", ->
      expect(@project.users().length).toEqual(2)

    it "should have one collaborator who is not the owner", ->
      expect(@project.collaborators().length).toEqual(1)
      expect(@project.collaborators()[0]).toEqual(@collaborator)

Collection

It seems we're testing backbone's callbacks here instead of our own code.

2. Test asynchronous code by stubbing out AJAX calls and timers

While Jasmine has stubs (plus spies and mock, Scenar.io uses sinon.js to stub callbacks. In this example, we verify that the model, after triggering a jQuery event, gets destroyed and continues with the callback:

describe "Scenario.Views.Projects.ProjectView", ->
  beforeEach ->
    @project = new Scenario.Models.Project()
    @view = new Scenario.Views.Projects.ProjectView({model: @project})

  describe "Project gets destroyed", ->
    beforeEach ->
      @destroyStub = sinon.stub Scenario.Models.Project.prototype, "destroy"
      @removeMeStub = sinon.stub Scenario.Views.Projects.ProjectView.prototype, "removeMe"

    it "should remove itself from the view after some time", ->
      $(@view.el).trigger("delete", new jQuery.Event())
      expect(@destroyStub).toHaveBeenCalled()
      expect(@removeMeStub).toHaveBeenCalled()
class Scenario.Views.Projects.ProjectView extends Backbone.View
    ...
    deleteProject: (e) ->
      e.preventDefault()
      self = @
      @model.destroy({
        success: self.removeMe(),
        error: console.log "Failed to destroy model!"
      })
    removeMe: ->
      # TODO Remove sub-views from the parent view (e.g. dimmer)
      console.log "Remove me!"

3. Test API calls to a Web service

Scenar.io uses a publish-subscribe service called Pusher. Our goal for the spec is to test Scenar.io's interactions with the Pusher API.

describe "Listening to project", ->
    beforeEach ->
      @channel =
        bind: (args) -> "bind"
      @pusher.subscribe =
        => return @channel
      @spy = sinon.spy @pusher, "subscribe"
      @handler = new Scenario.PusherHandler(@args)
      @handler.listen 'abcdefg'

    it "should subscribe to a channel", ->
      @spy.withArgs("presence-test@abcdefg")
      expect(@handler.channels['presence-test@abcdefg']).toBeDefined()
      expect(@spy.called).toBeTruthy()
class Scenario.PusherHandler
  listen: (project_id)->
    channel_id = "presence-"+@environment+"@"+project_id
    @channel = @channels[channel_id] # Set current channel
    unless @channel
      @channel = @pusher.subscribe(channel_id)
      @channel.bind "client-update", (msg) => @messageReceived(msg)
      @channel.bind 'pusher:subscription_succeeded', @subscriptionSucceeded
      @channel.bind 'pusher:subscription_error', -> @subscriptionError
      @channel.bind 'pusher:member_added', -> @memberAdded
      @channels[channel_id] = @channel

Observations, Advice, and Opinions

  • Stubs vs Spies vs Mocks
  • CoffeeScript: Thin arrow vs. Fat Arrow (scoping hack)
  • Write spec first? Sometimes. Benefits comes at not having to worry about events regressing.
  • Writing too much specs? Tendency to comment out blocks of specs. You'd have to ask, "why is this spec disabled?" A good rule of thumb is to ask: are you testing your code or backbone?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment