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 myTodo
items from the server into theToDoApp
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 myToDoApp
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 aTodosCollectionView
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
andTodosCollection
) 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 theitemViewContainer
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.