How I Test Backbone.Marionette with Jasmine

With the proliferation of client side apps and frameworks, testing strategies for writing client side code has become a major necessity. However, unlike most of the ruby community, effort in this department had been lacking…

Working on apps with zero client side tests, creates a series of unnecessary hurdles. It limits the developers ability to perfect the code, transfer knowledge to other developers, even pinpoint needed improvements.

Over the last few years though, I’ve developed a few techniques to counteract this lack of testing; by creating custom unit tests for Backbone.Marionette applications.

Notes on Backbone.Marionette testing:

  • Coffeescript will be utilized for the backbone code and tests.
  • Tests will be written using the Jasmine testing library
  • Backend for this app is a simple rails app
  • Jasminerice gem will compile coffeescript jasmine tests to javascript for browser.

    The Application:

    Alright, so you’ve got your requirements to build a very exciting to-do list application:

  • Ability to add new to-do items
  • Ability to mark to-do items as done
  • Ability to delete to-do items
  • For the sake of brevity, I will assume you have the basic setup and boot strap of a backbone application and are ready to render the view to your root route /.

    To quickly illustrate this, see index.html.erb file below. Notice it bootstraps my Todo items from the server into the ToDoApp backbone application.

    <div id="todos"></div>  <script type="text/javascript">   $(function() {      window.MyApp.start({todos: <%= @todos.to_json.html_safe %>});   }); </script> 

    Also, below is the main todo_app.js.coffee file for my ToDoApp backbone application:

    #= require_self #= require_tree ./templates #= require_tree ./models #= require_tree ./views #= require_tree ./routers  window.ToDoApp =   Models: {}   Collections: {}   Routers: {}   Views: {}  window.MyApp = new Backbone.Marionette.Application()  MyApp.addRegions   main: "#todos"  MyApp.addInitializer( (options) ->   todoView = new ToDoApp.Views.TodoCollectionView     collection: new ToDoApp.Collections.TodosCollection(options.todos)   MyApp.main.show(todoView) ) 

    As you can see, I am simply bootstrapping Todo objects from the server and rendering a TodosCollectionView into the #todos div.

    Test-Driving Functionality:

    Now to test drive our TodoCollectionView

    Starting with the tests, the functionality of this view enables you to create a new Todo item as well as render it. Thus, we can write tests assuming there will be some sort of text field and add button to create the item.

    Here’s a test file below:

    describe "ToDoApp.Views.TodoCollectionView", ->   describe "rendering", ->     describe "when there is a collection", ->       it "renders the collection", ->         collection = new ToDoApp.Collections.TodosCollection([           {id: 1, title: 'make example test', done: false}           {id: 2, title: 'make example work', done: false}         ])         view = new ToDoApp.Views.TodoCollectionView(collection: collection)         setFixtures(view.render().$el)         expect(view.children.length).toEqual(2)      describe "when there is not a collection", ->       it "renders the collection", ->         collection = new ToDoApp.Collections.TodosCollection([])         view = new ToDoApp.Views.TodoCollectionView(collection: collection)         setFixtures(view.render().$el)         expect(view.children.length).toEqual(0)    describe "events", ->     describe "click .add", ->       it "adds a new model to the collection", ->         view = new ToDoApp.Views.TodoCollectionView(collection: new ToDoApp.Collections.TodosCollection())         setFixtures(view.render().$el)         $('.add').click()         expect(view.collection.length).toEqual(1)        it "sets the new model's title from the text field", ->         view = new ToDoApp.Views.TodoCollectionView(collection: new ToDoApp.Collections.TodosCollection())         setFixtures(view.render().$el)         $('#new-todo').val("This is new")         $('.add').click()         expect(view.collection.models[0].get("title")).toEqual("This is new")        it "clears the value in the text field", ->         view = new ToDoApp.Views.TodoCollectionView(collection: new ToDoApp.Collections.TodosCollection())         setFixtures(view.render().$el)         $('#new-todo').val("This will be cleared")         $('.add').click()         expect($('#new-todo').val()).toEqual("") 

    When we run these tests (on the command line, for ease of copy + paste) we get:

    Run all Jasmine suites Run Jasmine suite at http://localhost:57702/jasmine Finished in 0.009 seconds ToDoApp.Views.TodoCollectionView   rendering     when there is a collection       ? renders the collection         ? TypeError: 'undefined' is not a constructor (evaluating 'new ToDoApp.Views.TodoCollectionView({ > [#]             collection: collection > [#]           })') in http://localhost:57702/assets/spec.js (line 16416)     when there is not a collection       ? renders the collection         ? TypeError: 'undefined' is not a constructor (evaluating 'new ToDoApp.Views.TodoCollectionView({ > [#]             collection: collection > [#]           })') in http://localhost:57702/assets/spec.js (line 16427)   events     click .add       ? adds a new model to the collection         ? TypeError: 'undefined' is not a constructor (evaluating 'new ToDoApp.Views.TodoCollectionView({ > [#]             collection: new ToDoApp.Collections.TodosCollection() > [#]           })') in http://localhost:57702/assets/spec.js (line 16439)       ? sets the new model's title from the text field         ? TypeError: 'undefined' is not a constructor (evaluating 'new ToDoApp.Views.TodoCollectionView({ > [#]             collection: new ToDoApp.Collections.TodosCollection() > [#]           })') in http://localhost:57702/assets/spec.js (line 16448)       ? clears the value in the text field         ? TypeError: 'undefined' is not a constructor (evaluating 'new ToDoApp.Views.TodoCollectionView({ > [#]             collection: new ToDoApp.Collections.TodosCollection() > [#]           })') in http://localhost:57702/assets/spec.js (line 16458) 5 specs, 5 failures Done. Guard::Jasmine stops server. rake aborted! Some specs have failed 

    We got one hundred perecent failure because we haven’t created our TodoCollectionView yet. Let’s do that:

    class ToDoApp.Views.TodoCollectionView extends Backbone.Marionette.CompositeView 

    Quick note: The backbone model and collection (Todo and TodosCollection) were pre-made. Thus, since there is no functionality outside of backbone, and therefore no tests, I’ll illustrate here…

    class ToDoApp.Models.Todo extends Backbone.Model   paramRoot: 'todo'    defaults:     title: null     done: null  class ToDoApp.Collections.TodosCollection extends Backbone.Collection   model: ToDoApp.Models.Todo   url: '/todos' 

    Now re-run the tests.

    Run all Jasmine suites Run Jasmine suite at http://localhost:57732/jasmine Finished in 0.011 seconds ToDoApp.Views.TodoCollectionView   rendering     when there is a collection       ? renders the collection         ? TemplateNotFoundError: Cannot render the template since it's false, null or undefined. in http://localhost:57732/assets/spec.js (line 14154)     when there is not a collection       ? renders the collection         ? TemplateNotFoundError: Cannot render the template since it's false, null or undefined. in http://localhost:57732/assets/spec.js (line 14154)   events     click .add       ? adds a new model to the collection         ? TemplateNotFoundError: Cannot render the template since it's false, null or undefined. in http://localhost:57732/assets/spec.js (line 14154)       ? sets the new model's title from the text field         ? TemplateNotFoundError: Cannot render the template since it's false, null or undefined. in http://localhost:57732/assets/spec.js (line 14154)       ? clears the value in the text field         ? TemplateNotFoundError: Cannot render the template since it's false, null or undefined. in http://localhost:57732/assets/spec.js (line 14154) 5 specs, 5 failures Done. Guard::Jasmine stops server. rake aborted! Some specs have failed 

    At this stage, while all failed, we are getting better errors. Let’s try giving it a template to render (JST was utilized for template):

    class ToDoApp.Views.TodoCollectionView extends Backbone.Marionette.CompositeView   template: JST['backbone/templates/todos/index'] 

    and the template file:

    <div id="new">   <input id="new-todo" type="text" placeholder="new item" />   <button class='add'>Add</button> </div>  <ul id="todos"> </ul> 

    As you can see, I skipped ahead and put in the text field, add button, and container for the Todo items. Now, let’s rerun the tests.

    Run all Jasmine suites Run Jasmine suite at http://localhost:57764/jasmine Finished in 0.026 seconds ToDoApp.Views.TodoCollectionView   events     click .add       ? adds a new model to the collection         ? Expected 0 to equal 1.       ? sets the new model's title from the text field         ? TypeError: 'undefined' is not an object (evaluating 'view.collection.models[0].get') in http://localhost:57764/assets/spec.js (line 16467)       ? clears the value in the text field         ? Expected 'This will be cleared' to equal ''. 5 specs, 3 failures Done. Guard::Jasmine stops server. rake aborted! Some specs have failed 

    Nice! Looks like a couple of our tests passed this time; namely the tests about rendering the view with a collection. However, our add button clicking tests still failed.

    One at a time, let’s make them pass. First priority, making sure clicking the add button adds a model to the collection:

    class ToDoApp.Views.TodoCollectionView extends Backbone.Marionette.CompositeView   template: JST['backbone/templates/todos/index']    ui:     newTitle: "#new-todo"    events:     "click .add" : "addNewTodoItem"    addNewTodoItem: ->     @collection.create(new ToDoApp.Models.Todo(title: @ui.newTitle.val())) 

    Re-run the tests.

    Run all Jasmine suites Run Jasmine suite at http://localhost:57808/jasmine Finished in 0.027 seconds ToDoApp.Views.TodoCollectionView   events     click .add       ? clears the value in the text field         ? Expected 'This will be cleared' to equal ''. 5 specs, 1 failure Done. Guard::Jasmine stops server. rake aborted! Some specs have failed 

    Cool, only one failure left, and it looks like simply clearing the input field, after creation of a new Todo item, should do the trick. Here’s the code for that:

    class ToDoApp.Views.TodoCollectionView extends Backbone.Marionette.CompositeView   template: JST['backbone/templates/todos/index']    ui:     newTitle: "#new-todo"    events:     "click .add" : "addNewTodoItem"    addNewTodoItem: ->     @collection.create(new ToDoApp.Models.Todo(title: @ui.newTitle.val()))     @ui.newTitle.val("") 

    Re-run our tests.

    Finished in 0.025 seconds ToDoApp.Views.TodoCollectionView   rendering     when there is a collection       ? renders the collection     when there is not a collection       ? renders the collection   events     click .add       ? adds a new model to the collection       ? sets the new model's title from the text field       ? clears the value in the text field 5 specs, 0 failures Done. 

    Hoorah, all green!

    Alright, only one more housekeeping task. If you were following along, you may have noticed the application doesn’t work as expected in browser view. This is because we have not declared what the ItemView for this collection is supposed to be…

    Let’s write a test that drives this functionality out:

    it "renders TodoViews as the itemViews", ->   collection = new ToDoApp.Collections.TodosCollection([     {id: 1, title: 'make example test', done: false}   ])   view = new ToDoApp.Views.TodoCollectionView(collection: collection   view.render().$el   expect(view.children.first().constructor.name).toEqual("TodoView") 

    Re-run the tests that fail.

    Expected 'TodoCollectionView' to equal 'TodoView'. 

    Next, lets make sure we set the ItemView property of our collection view (as well as the itemViewContainer property for good measure):

    class ToDoApp.Views.TodoCollectionView extends Backbone.Marionette.CompositeView   template: JST['backbone/templates/todos/index']   itemView: ToDoApp.Views.TodoView   itemViewContainer: "#todos"    ui:     newTitle: "#new-todo"    events:     "click .add" : "addNewTodoItem"    addNewTodoItem: ->     @collection.create(new ToDoApp.Models.Todo(title: @ui.newTitle.val()))     @ui.newTitle.val("") 

    Re-run our tests.

    Expected 'TodoCollectionView' to equal 'TodoView'. 

    Oh no! We get the same failure, what happened? Well, we never made our TodoView view. Let’s do that now (see below):

    View:

    class ToDoApp.Views.TodoView extends Backbone.Marionette.ItemView   template: JST['backbone/templates/todos/_todo']   tagName: 'li' 

    Template:

    <p><%= title %></p> <p><%= done %></p> 

    Cool, now let’s re-run our tests.

    Finished in 0.026 seconds ToDoApp.Views.TodoCollectionView   rendering     when there is a collection       ? renders the collection       ? renders TodoViews as the itemViews     when there is not a collection       ? renders the collection   events     click .add       ? adds a new model to the collection       ? sets the new model's title from the text field       ? clears the value in the text field 6 specs, 0 failures Done. 

    Ta-da! 100% success! We now have a small backbone app, fully tested and fully functioning, that allows you to create and list todo items.

    Want to learn more about client-side testing? Let us know! We’d love to hear from you.