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:
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)
It seems we're testing backbone's callbacks here instead of our own code.
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!"
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
- 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?