lunes, 27 de agosto de 2012

Building and Testing a Backbone App

In this tutorial, we’ll develop a Backbone.js application, while testing it with Jasmine. Not good enough for you? We’ll do it all using CoffeeScript. Trifecta!

We’re going to work on the application in isolation – using a static, serverless environment. This has multiple advantages:

  • Testing and running code is extremely fast.
  • Decoupling our Backbone application from the server side makes it just another client. We could build a mobile application, for example, that would consume the same API.

Our test application will be a simple website where we can manage a database containing nothing more than restaurants.


Starting Boilerplate

To start, we need to move a few pieces into place. Simply download this tarball that contains:

There are also two HTML files: index.html and SpecRunner.html. The former shows our app running, while the latter runs our Jasmine specs.

Let’s test our setup by running the application through a web server. There are various options for this, but I usually rely on a very simple Python command (available on OsX):

  python -m SimpleHTTPServer  

Backbone provides a nice API to define events in the scope of a specific view.

Next, navigate your browser to http://localhost:8000/index.html, and you should see a congratulations message. Also open http://localhost:8000/SpecRunner.html; the page should contain a sample spec running green.

You should also find a Cakefile in the root directory. This is a very simple CoffeeScript file that you can use to automatically compile all the .coffee files we’re going to write. It assumes that you have CoffeeScript installed as a globally available Node module, and you can refer to this page for instructions. Alternatively, you can use tools like CodeKit or Livereload to accomplish the same result.

To run the cake task, just type cake compile. This task will keep running. You can watch for changes every time you save, but you may need to restart the script if you add new files.


Step 1 – The Restaurant Model

Namespacing

Using Backbone means we’re going to create models, collections and views. Therefore, having a namespace to keep them organized is a good practice, and we can do that by creating an app file and a relevant spec:

  touch javascript/app.coffee  touch javascript/spec/app_spec.coffee  

The spec file contains just one test:

  describe "App namespace", ->      it "should be defined", ->      expect(Gourmet).toBeDefined()  

Switching to the javascript/app.coffee file, we can add the following namespace declaration:

  window.Gourmet =    Models: {}    Collections: {}    Views: {}  

Next, we need to add the app file to index.html:

  ...  <script type="text/javascript" src="/javascript/app.js"></script>  ...  

We need to do the same in SpecRunner.html, but this time for both app and spec:

  <!-- lib -->  <script type="text/javascript" src="/javascript/app.js"></script>    <!-- specs -->  <script type="text/javascript" src="/javascript/spec/toolchain_spec.js"></script>  <script type="text/javascript" src="/javascript/spec/app_spec.js"></script>  

Repeat this for every file we create from now on.

Basic Attributes

The core entity of our app is a restaurant, defined by the following attributes:

  • a name
  • a postcode
  • a rating (1 to 5)

As adding more attributes would not provide any advantages in the scope of the tutorial, we can just work with these three for now.

Let’s create the Restaurant model and the relevant spec file:

  mkdir -p javascript/models/  mkdir -p javascript/spec/models/  touch javascript/models/restaurant.coffee  touch javascript/spec/models/restaurant_spec.coffee  

Now we can open both files and add some basic specs to restaurant_spec.coffee, shown here:

  describe "Restaurant Model", ->      it "should exist", ->      expect(Gourmet.Models.Restaurant).toBeDefined()      describe "Attributes", ->        ritz = new Gourmet.Models.Restaurant        it "should have default attributes", ->        expect(ritz.attributes.name).toBeDefined()        expect(ritz.attributes.postcode).toBeDefined()        expect(ritz.attributes.rating).toBeDefined()  

The test is very simple:

  • We check that a Restaurant class exists.
  • We also check that a new Restaurant instance is always initialized with defaults that mirror the requirements we have.

Refreshing /SpecRunner.html will show the specs failing. Now let’s implement models/restaurant.coffee. It’s even shorter:

  class Gourmet.Models.Restaurant extends Backbone.Model      defaults:      name: null      postcode: null      rating: null  

Backbone will take care of sending the correct Ajax requests.

We just need to create a class on the window namespace to make it globally available–we’ll will worry about the namespace in the second part. Now, our specs should pass. Refresh /SpecRunner.html, and the specs should pass.

Validations

As I said before, we will use Backbone Validations for client side validation. Let’s add a new describe block to models/restaurant_spec.coffee to express our expectations:

  describe "Restaurant Model", ->  ...    describe "Validations", ->        attrs = {}        beforeEach ->        attrs =          name: 'Ritz'          postcode: 'N112TP'          rating: 5        afterEach ->        ritz = new Gourmet.Models.Restaurant attrs        expect(ritz.isValid()).toBeFalsy()        it "should validate the presence of name", ->        attrs["name"] = null        it "should validate the presence of postcode", ->        attrs["postcode"] = null        it "should validate the presence of rating", ->        attrs["rating"] = null        it "should validate the numericality of rating", ->        attrs["rating"] = 'foo'        it "should not accept a rating < 1", ->        attrs["rating"] = 0        it "should not accept a rating > 5", ->        attrs["rating"] = 6  

We define an empty attributes object that will be modified in every expectation. Each time we will set only one attribute with an invalid value, thus testing the thoroughness of our validation rules. We can also use an afterEach block to avoid a lot of repetition. Running our specs will show 6 failures. Once again, we have an extremely concise and readable implementation, thanks to Backbone validations:

  class Gourmet.Models.Restaurant extends Backbone.Model      defaults:      name: null      postcode: null      rating: null      validate:      name:        required: true      postcode:        required: true      rating:        required: true        type:     'number'        min:      1        max:      5  

Our specs will now pass, and with these changes in place, we have a quite solid Restaurant model.

The Restaurants Collection

Because we want to manage a list of restaurants, it makes sense to have a RestaurantsCollection class. We don’t know yet how complicated it needs to be; so, let’s focus on the bare minimum requirements by adding a new describe block to the models/restaurant_spec.coffee file:

  describe "Restaurant model", ->  ...  describe "Restaurants collection", ->      restaurants = new Gourmet.Collections.RestaurantsCollection      it "should exist", ->      expect(Gourmet.Collections.RestaurantsCollection).toBeDefined()      it "should use the Restaurant model", ->      expect(restaurants.model).toEqual Gourmet.Models.Restaurant  

Backbone provides an extensive list of methods already defined for a collection, so our work here is minimal. We don’t want to test methods defined by the framework; so, we just have to make sure that the collection uses the right model. Implementation-wise, we can append the following few lines to models/restaurant.coffee:

  class Gourmet.Collections.RestaurantsCollection extends Backbone.Collection      model: Gourmet.Models.Restaurant  

It’s clear that CoffeeScript and Backbone are a very powerful team when it comes to clarity and conciseness. Let’s rerun our specs to verify that everything’s green.


Step 2 – The Restaurants View

The Markup

Until now, we haven’t even looked at how we’re going to display or interact with our data. We’ll keep it visually simple and focus on two actions: adding and removing a restaurant to/from the list.

Thanks to Bootstrap, we can easily add some basic markup that results in a decent looking prototype table. Let’s open the index.html file and add the following body content:

  <div class="container">    <div class="navbar">      <div class="navbar-inner">        <div class="container">          <a href="#" class="brand">Awesome restaurants</a>        </div>      </div>    </div>    <div class="container">      <div class="row">        <div class="span4">          <form action="#" class="well form-horizontal" id="restaurant-form">            <div class="control-group">              <label for="restaurant_name">Name</label>              <input type="text" name="restaurant[name]" id="restaurant_name" />              <span class="help-block">Required</span>            </div>            <div class="control-group">              <label for="restaurant_rating">Rating</label>              <input type="text" name="restaurant[rating]" id="restaurant_rating" />              <span class="help-block">Required, only a number between 1 and 5</span>            </div>            <div class="control-group">              <label for="restaurant_postcode">Postcode</label>              <input type="text" name="restaurant[postcode]" id="restaurant_postcode" />              <span class="help-block">Required</span>            </div>            <input type="button" class="btn btn-primary" value="Save" id="save"/>          </form>        </div>        <div class="span8">          <table class="table" id="restaurants">            <thead>              <tr>                <th>Name</th>                <th>Postcode</th>                <th>Rating</th>              </tr>            </thead>            <tbody></tbody>          </table>        </div>      </div>    </div>  </div>  

What we really care about is the #restaurant-form and the #restaurants table. The input elements use a conventional pattern for their names (entity[attribute]), making them easily processable by most back-end frameworks (especially Rails). As for the table, we are leaving the tbody empty, as we will render the content on the client with Hogan. In fact, we can add the template we’re going to use right before all other <script> tags in the <head>.

  ...  <link rel="stylesheet" media="screen" href="/css/bootstrap.css" >  <script type="text/mustache" id="restaurant-template">    <tr>      <td>{{ name }}</td>      <td>{{ postcode }}</td>      <td>{{ rating }}</td>      <td>        <i class="icon-remove remove" id="{{ id }}"></i>      </td>    </tr>  </script>  <script type="text/javascript" src="/javascript/vendor/jquery.min.js"></script>  ...  

Being a Mustache template, it needs the correct text/mustache type and an id we can use to retrieve it from the DOM. All the parameters enclosed in {{ }} are attributes of our Restaurant model; this simplifies the rendering function. As a last step, we can add a remove icon that, when clicked, deletes the corresponding restaurant.

The Restaurants View Class

As previously stated, we have two core view components: the restaurants list and the restaurant form. Let’s tackle the first by creating both the directory structure for views and the needed files:

  mkdir -p javascript/views  mkdir -p javascript/spec/views  touch javascript/views/restaurants.coffee  touch javascript/spec/views/restaurants_spec.coffee  

Let’s also copy #restaurant-template to the SpecRunner.html file:

  ...  <script type="text/javascript" src="/javascript/vendor/jasmine-jquery.js"></script>    <!-- templates -->  <script type="text/mustache" id="restaurant-template">    <tr>      <td>{{ name }}</td>      <td>{{ postcode }}</td>      <td>{{ rating }}</td>      <td>        <i class="icon-remove remove" id="{{ id }}"></i>      </td>    </tr>  </script>    <!-- vendor js -->  <script type="text/javascript" src="/javascript/vendor/jquery.min.js"></script>  ...  

In addition, we need to include the .js files in the head of SpecRunner.html. We can now open views/restaurant_spec.coffee and start editing.

  describe "Restaurants view", ->      restaurants_data = [      {        id: 0        name: 'Ritz'        postcode: 'N112TP'        rating: 5      },      {        id: 1        name: 'Astoria'        postcode: 'EC1E4R'        rating: 3      },      {        id: 2        name: 'Waldorf'        postcode: 'WE43F2'        rating: 4      }    ]      invisible_table = document.createElement 'table'      beforeEach ->      @restaurants_collection = new Gourmet.Collections.RestaurantsCollection restaurants_data      @restaurants_view = new Gourmet.Views.RestaurantsView        collection: @restaurants_collection        el: invisible_table      it "should be defined", ->      expect(Gourmet.Views.RestaurantsView).toBeDefined()      it "should have the right element", ->      expect(@restaurants_view.el).toEqual invisible_table      it "should have the right collection", ->      expect(@restaurants_view.collection).toEqual @restaurants_collection  

Fixtures are a simple way to import HTML fragments in our tests without having to write them inside the spec file itself.

It looks like a lot of code, but this is a standard start for a view spec. Let’s walk through it:

  • We begin by instantiating an object that holds some restaurant data. As suggested by the Backbone documentation, it’s a good practice to feed a Backbone app the data it needs directly in the markup to avoid a delay for the user and an extra HTTP request when the page opens.
  • We create an invisible table element without appending it to the DOM; we don’t need it for user interaction.
  • We define a beforeEach block where we instantiate a RestaurantsCollection with the data we created before. Doing it in a beforeEach block guarantees that every spec will start with a clean slate.
  • We then instantiate a RestaurantsView class and pass both the collection and the invisible table in the initializer. The object keys, collection and el, are default Backbone methods for a View class. They identify the container where the view will be rendered and the data source used to populate it.
  • The specs simply check that everything we assume in the beforeEach block is true.

Running our tests throws an error because the RestaurantsView class is not yet defined. We can easily get everything to green by adding the following content to views/restaurant.coffee:

  class Gourmet.Views.RestaurantsView extends Backbone.View  

We don’t need to override or change the constructor defined by the Backbone.View prototype because we instantiated the view with a collection and an el attribute. This single line is enough to get our specs green; it will, however, do pretty much nothing from the end result point of view.

Assuming there are restaurants added to the collection, the view class should render them on the page as soon as the page loads. Let’s translate this requirement into a spec that we can add at the bottom of the views/restaurant_spec.coffee file:

  it "should render the the view when initialized", ->    expect($(invisible_table).children().length).toEqual 3  

We can test the number of children (<tr/> elements) that the invisible table needs to have, considering that we have defined a sample dataset of three restaurants. This will result in a red spec because we haven’t even started working on rendering. Let’s add the relevant piece of code to the RestaurantsView class:

  class Gourmet.Views.RestaurantsView extends Backbone.View    template: Hogan.compile $('#restaurant-template').html()    initialize: ->      @render @collection    render: =>      @$el.empty()      for restaurant in @collection.models        do (restaurant) =>          @$el.append @template.render(restaurant.toJSON())  

…the real benefit is the possibility to work effectively on testable pieces of functionality that follow predictable patterns.

You will see this pattern very frequently in a Backbone application, but let’s break it into pieces:

  • The template function isolates the templating logic we use inside the application. We’re using mustache templates compiled through Hogan, but we could’ve used Underscore or Mustache itself. All of them follow a similar API structure; so, switching would not be difficult (albeit a bit boring). In addition, isolating the template function gives a clear idea of which template a view uses.
  • The render function empties the el (note that @$el is a cached, jQuery wrapped version of the element itself made available by default by Backbone), iterates on the models inside the collection and render the result, and appends it to the element. This is a naive implementation, and you may want to refactor it to append just once instead of doing it at every loop.
  • Finally, we call render when the view is initialized.

This will make our spec green and will give us a minimal amount of code useful to actually show it on the page. Let’s open index.html and add the following:

  ...  <body>    <script type="text/javascript">      restaurants_data = [        {          id: 0,          name: 'Ritz',          postcode: 'N112TP',          rating: 5        },        {          id: 1,          name: 'Astoria',          postcode: 'EC1E4R',          rating: 3        },        {          id: 2,          name: 'Waldorf',          postcode: 'WE43F2',          rating: 4        }      ];      $(document).ready(function(){        restaurants = new Gourmet.Collections.RestaurantsCollection(restaurants_data);        restaurants_view = new Gourmet.Views.RestaurantsView({          collection: restaurants,          el: '#restaurants tbody'        })      });    </script>    ...  

We’re basically replicating the default dataset and the setup needed to get the app running. We’re also doing it inside the HTML file because this code is useful only in this static version of the app.

Refresh the page and behold! The restaurants table will be populated with results.

Our restaurants home

Next, we need to handle what happens when we add or remove a restaurant from the collection. It’s important to remember that the form is just one possible way to act on the collection; we could also have push events from other users, for example. Therefore, it is essential that this logic is separated in a clean and independent manner.

What do we expect to happen? Let’s add this specs to the views/restaurants\_view\_spec.coffee file (right after the last one):

  it "should render when an element is added to the collection", ->    @restaurants_collection.add      name: 'Panjab'      postcode: 'N2243T'      rating: 5    expect($(invisible_table).children().length).toEqual 4    it "should render when an element is removed from the collection", ->    @restaurants_collection.pop()    expect($(invisible_table).children().length).toEqual 2  

In essence, we add and remove a restaurant to the collection, expecting our table to update itself accordingly. Adding this behavior to the view class requires a couple of lines in the initializer, as we can leverage on Backbone events on the collection:

  ...  initialize: ->    @render @collection    @collection.on 'add', @render    @collection.on 'remove', @render  ...  

We can re-render the whole table using the collection in the current state (after an element has been added or removed) because our rendering logic is pretty simple. This will make our specs to pass.

When you now open the index.html file, you will see that the remove icon on each table row doesn’t do anything. Let’s spec out what we expect to happen at the end of the views/restaurants\_view\_spec.coffee file:

  it "should remove the restaurant when clicking the remove icon", ->    remove_button = $('.remove', $(invisible_table))[0]    $(remove_button).trigger 'click'    removed_restaurant = @restaurants_collection.get remove_button.id    expect(@restaurants_collection.length).toEqual 2    expect(@restaurants_collection.models).not.toContain removed_restaurant  

Jasmine spies are quite powerful, and I encourage you to read about them.

The test is pretty verbose, but it summarizes exactly what needs to happen:

  • We find the remove icon of the first row in the table with jQuery.
  • We then click that icon.
  • We identify which restaurant needs to be removed by using the id of the remove button, which corresponds to the id of the restaurant model.
  • We test that the restaurants collection has an element less, and that element is exactly the one we identified before.

How can we implement this? Backbone provides a nice API to define events in the scope of a specific view. Let’s add one to the RestaurantsView class:

  class Gourmet.Views.RestaurantsView extends Backbone.View    events:      'click .remove': 'removeRestaurant'    ...    removeRestaurant: (evt) =>      id = evt.target.id      model = @collection.get id      @collection.remove model  

When clicking on an element with class .remove, the view calls the removeRestaurant function and passes the jQuery event object. We can use it to get the id of the element and remove the relevant model from the collection. We already handle what happens when removing an element from the collection; so, this will be enough to get the spec to green.

In addition, you can open index.html and see it in action in the browser.

The Restaurant Form Class

We now need to handle the user input when using the form to add a new restaurant:

  • If the user inputs invalid data, we’re going to display inline validation errors.
  • If the user inputs valid data, the restaurant will be added to the collection and displayed in the table.

As we’ve already added validations to the Restaurant model, we now need to wire them to the view. Not surprisingly, we will start by creating a new view class and the relevant spec file.

  touch javascript/views/restaurant_form.coffee  touch javascript/spec/views/restaurant\_form\_spec.coffee  

Once again, let’s remember to add the JavaScript compiled version of the view to index.html and both compiled versions to SpecRunner.html.

It’s a good time to introduce fixtures, a piece of functionality made available by Jasmine-jQuery, because we will be dealing with the form markup. In essence, fixtures are a simple way to import HTML fragments in our tests without having to write them inside the spec file itself. This keeps the spec clean, understandable, and can eventually lead to reusability of the fixture among multiple specs. We can create a fixture for the form markup:

  mkdir -p javascript/spec/fixtures  touch javascript/spec/fixtures/restaurant_form.html  

Let’s copy the whole form in index.html to the restaurant_form.html fixture:

  <form action="#" class="well form-horizontal" id="restaurant-form">    <div class="control-group">      <label for="restaurant_name">Name</label>      <input type="text" name="restaurant[name]" id="restaurant_name" />      <span class="help-block">Required</span>    </div>    <div class="control-group">      <label for="restaurant_rating">Rating</label>      <input type="text" name="restaurant[rating]" id="restaurant_rating" />      <span class="help-block">Required, only a number between 1 and 5</span>    </div>    <div class="control-group">      <label for="restaurant_postcode">Postcode</label>      <input type="text" name="restaurant[postcode]" id="restaurant_postcode" />      <span class="help-block">Required</span>    </div>    <input type="button" class="btn btn-primary" value="Save" id="save"/>  </form>  

Now open views/restaurant\_form\_spec.coffee and add the fixture along with some boilerplate:

  describe "Restaurant Form", ->      jasmine.getFixtures().fixturesPath = 'javascript/spec/fixtures'      beforeEach ->      loadFixtures 'restaurant_form.html'      @invisible_form = $('#restaurant-form')      @restaurant_form = new Gourmet.Views.RestaurantForm        el: @invisible_form        collection: new Gourmet.Views.RestaurantsCollection      it "should be defined", ->      expect(Gourmet.Views.RestaurantForm).toBeDefined()      it "should have the right element", ->      expect(@restaurant_form.$el).toEqual @invisible_form      it "should have a collection", ->      expect(@restaurant_form.collection).toEqual (new Gourmet.Views.RestaurantsCollection)  

The jasmine.getFixtures().fixtures_path attribute change is needed as we have a custom directory structure that differs from the library default. Then, in the beforeEach block, we load the fixture and define an @invisible_form variable that targets the form we just imported. Finally, we define an instance of the class we’re going to create, passing in an empty restaurants collection and the @invisible_form we just created. As usual, this spec will be red (the class is still undefined), but if we open restaurant_form.coffee we can easily fix it:

      class Gourmet.Views.RestaurantForm extends Backbone.View  

Next, we need to think about our spec’s structure. We have two choices:

Using Backbone means we’re going to create models, collections and views. Therefore, having a namespace to keep them organized is a good practice

  • We can spy on the form content with jasmine and mock it.
  • We could manually change the content of the fields and then simulate a click.

Personally, I favor the first approach. The second would not eliminate the need for proper integration testing, but it would increase the complexity of the spec.

Jasmine spies are quite powerful, and I encourage you to read about them. If you come from a Ruby testing background, they’re very similar to RSpec’s mocks and feel very familiar. We do need to have an idea of the pattern we are going to implement, at least with broad strokes:

  • The user enters data in the form.
  • When he presses save, we get the form content in a serialized form.
  • We transform that data and create a new restaurant in the collection.
  • If the restaurant is valid, we save it, otherwise we will display validation errors.

As said before, we’re going to mock the first step, and we’ll do so by defining a new describe block where we instantiate an object that represents a well formed, valid data structure coming from a form.

  describe "Restaurant Form", ->  ...    describe "form submit", ->        beforeEach ->        @serialized_data = [          {            name: 'restaurant[name]',            value: 'Panjab'          },          {            name: 'restaurant[rating]',            value: '5'          },          {            name: 'restaurant[postcode]',            value: '123456'          }        ]        spyOn(@restaurant_form.$el, 'serializeArray').andReturn @serialized_data  

At the end, we define a spy on the serializeArray method for our form. That means that if we call @restaurant_form.$el.serializeArray(), we already know that it’s going to return the object we created above. This is the mocking facility we needed; it simulates the user input we need to test with. Next, we can add some specs:

  it "should parse form data", ->    expect(@restaurant_form.parseFormData(@serialized_data)).toEqual      name: 'Panjab',      rating: '5',      postcode: '123456'    it "should add a restaurant when form data is valid", ->    spyOn(@restaurant_form, 'parseFormData').andReturn      name: 'Panjab',      rating: '5',      postcode: '123456'    @restaurant_form.save() # we mock the click by calling the method    expect(@restaurant_form.collection.length).toEqual 1    it "should not add a restaurant when form data is invalid", ->    spyOn(@restaurant_form, 'parseFormData').andReturn      name: '',      rating: '5',      postcode: '123456'    @restaurant_form.save()    expect(@restaurant_form.collection.length).toEqual 0    it "should show validation errors when data is invalid", ->    spyOn(@restaurant_form, 'parseFormData').andReturn      name: '',      rating: '5',      postcode: '123456'    @restaurant_form.save()    expect($('.error', $(@invisible_form)).length).toEqual 1  

In the first spec, we verify that our RestaurantForm class has a method that parses the data from the form. This method should return an object that we can feed to the restaurant collection. In the second spec, we mock the previous method because we don’t need to test it again. Instead, we focus on what happens when the user clicks ‘Save’. It will probably trigger an event that calls a save function.

We should tweak the second spec’s mock to return invalid data for a restaurant in order to verify that the restaurant doesn’t get added to the collection. In the third spec, we verify that this also triggers validation errors in the form. The implementation is somewhat tricky:

  class Gourmet.Views.RestaurantForm extends Backbone.View      events:      'click #save': 'save'      save: ->      data = @parseFormData(@$el.serializeArray())      new_restaurant = new Restaurant data      errors = new_restaurant.validate(new_restaurant.attributes)      if errors then @handleErrors(errors) else @collection.add new_restaurant      parseFormData: (serialized_array) ->      _.reduce serialized_array, @parseFormField, {}      parseFormField: (collector, field_obj) ->      name = field_obj.name.match(/\[(\w+)\]/)[1]      collector[name] = field_obj.value      collector      handleErrors: (errors) ->      $('.control-group').removeClass 'error'      for key in (_.keys errors)        do (key) ->          input = $("#restaurant_#{key}")          input.closest('.control-group').addClass 'error'  

This is a good practice to make sure that we use the fake server only where we need to, minimizing interference with the rest of the test suite.

Let’s see each function:

  • We have an events hash that binds the user’s mouse click to a save function.
  • The save function parses the data (more on that below) in the form and creates a new restaurant. We call the validate function (available by Backbone and defined by Backbone-validations). It should return false when the model is valid, and an error object when it’s invalid. If valid, we add the restaurant to the collection.
  • The two ‘parse’ functions are needed to extract the attribute names from the form and create an object in the desired Backbone-ready format. Bear in mind that this complexity is needed because of the markup. We could change it, but this is a good example of how you could work on top of an existing form to enhance it.
  • The handleErrors function iterates over the errors object and finds the corresponding input fields, adding the .error class when appropriate.

Running the specs now shows a reassuring series of green dots. To have it running in the browser, we need to extend our initialize function:

  $(document).ready(function(){    restaurants = new Gourmet.Collections.RestaurantsCollection(restaurants_data);    restaurants_view = new Gourmet.Views.RestaurantsView({      collection: restaurants,      el: '#restaurants tbody'    });    restaurant\_form\_view = new Gourmet.Views.RestaurantForm({      el: '#restaurant-form',      collection: restaurants    });  });  

There’s only one caveat: for now you can’t delete a restaurant that you added because we rely on the id attribute to target the correct model in the restaurants collection (Backbone needs a persistence layer to assign it). This is where you would add, depending on your needs, a real back-end–like a Rails server or a LocalStorage adapter.


Step 3 – Testing server interaction

Even though we’re on a server-less environment, we can take advantage of a couple of extra libraries that let us wire up our application for a server deploy. As a proof of concept, we will assume to be working on top of a Ruby on Rails stack.

To use Backbone with a Rails application, we need to have an additional adapter for syncing; Backbone doesn’t provide that by default (it’s a server agnostic tool). We can use the one included in the Backbone-rails project.

  curl -o javascript/vendor/backbone\_rails\_sync.js http://raw.github.com/codebrew/backbone-rails/master/vendor/assets/javascripts/backbone\_rails\_sync.js  

Next, we need to include it both in index.html and SpecRunner.html, right after the script that requires Backbone itself. This adapter takes care of executing all the asyncronous requests we need, provided that we setup our Restaurant model and our RestaurantsCollection with the right URLs.

How are we going to test this? We can use Sinon.js, a very powerful JavaScript mocking library that is also able to instantiate a fake server object that will intercept all XHR requests. Once again, we can simply:

  curl -o javascript/vendor/sinon.js http://sinonjs.org/releases/sinon-1.4.2.js  

Don’t forget to add it to the SpecRunner.html file right after Jasmine.

Now we can start thinking about the server API. We can assume it follows a RESTful architecture (a direct consequence of choosing Rails as a backend) and uses the JSON format. Because we’re managing restaurants, we can also assume that the base URL for every request will be /restaurants.

We can add two specs to the models/restaurant_spec.coffee file to make sure that both collection and model are properly setup:

  ...    it "should have default attributes", ->    expect(ritz.attributes.name).toBeDefined()    expect(ritz.attributes.postcode).toBeDefined()    expect(ritz.attributes.rating).toBeDefined()    it "should have the right url", ->    expect(ritz.urlRoot).toEqual '/restaurants'    ...    it "should use the Restaurant model", ->    expect(restaurants.model).toEqual Gourmet.Models.Restaurant    it "should have the right url", ->    expect(restaurants.url).toEqual '/restaurants'  

To implement this, we need to define two methods on the Restaurant model and the RestaurantsCollection class:

  class Gourmet.Models.Restaurant extends Backbone.Model      urlRoot: '/restaurants'    ...    class Gourmet.Collections.RestaurantsCollection extends Backbone.Collection      url: '/restaurants'      model: Gourmet.Models.Restaurant  

Watch out for the different method name!

Decoupling our Backbone application from the server side makes it just another client.

This is what is needed to setup server integration. Backbone will take care of sending the correct Ajax requests. Fore example, creating a new restaurant triggers a POST request to /restaurants with the new restaurant attributes in JSON format. As these requests are always the same (that is guaranteed by the rails_sync adapter), we can reliably test that interaction on the page will trigger those requests.

Let’s open the views/restaurants_spec.coffee file and setup Sinon. We will use its fakeServer facility to check the requests sent to the server. As a first step, we have to instantiate a sinon server in a beforeEach block. We will also need to make sure to restore the normal functionality right after running our specs. This is a good practice to make sure that we use the fake server only where we need to, minimizing interference with the rest of the test suite.

    beforeEach ->      @server = sinon.fakeServer.create()      @restaurants_collection = new Gourmet.Collections.RestaurantsCollection restaurants_data      @restaurants_view = new Gourmet.Views.RestaurantsView        collection: @restaurants_collection        el: invisible_table      afterEach ->      @server.restore()  

Next, we add a spec to test that a DELETE request is sent to the server when we press the remove icon for a restaurant:

  it "should remove a restaurant from the collection", ->    evt = { target: { id: 1 } }    @restaurants_view.removeRestaurant evt    expect(@restaurants_collection.length).toEqual 2    it "should send an ajax request to delete the restaurant", ->    evt = { target: { id: 1 } }    @restaurants_view.removeRestaurant evt    expect(@server.requests.length).toEqual 1    expect(@server.requests[0].method).toEqual('DELETE')    expect(@server.requests[0].url).toEqual('/restaurants/1')  

We can easily inspect @server.requests, an array of all the XHR requests made in the test. We check protocol and URL of the first request and ensure it matches with the expectation. If you run the spec, it will fail because our current logic simply removes the restaurant from the collection without deleting it. Let’s open views/restaurants.coffee and revise the removeRestaurant method:

  removeRestaurant: (evt) =>    id = evt.target.id    model = @collection.get id    @collection.remove model    model.destroy()  

By calling destroy, we effectively trigger the DELETE request, making our spec pass.

Next up, the restaurant form. We want to test that every time a form with valid data is submitted, a POST request is sent to the server with the correct data. We will also refactor our tests to isolate valid and invalid attributes in two variables; this will reduce the amount of repetition that we already have. For clarity, here is the full Form submit block from views/restaurant\_form\_spec.coffee:

  describe "Form submit", ->      # attrs need to be alphabetical ordered!    validAttrs =      name: 'Panjab',      postcode: '123456',      rating: '5'      invalidAttrs =      name: '',      postcode: '123456',      rating: '5'      beforeEach ->      @server = sinon.fakeServer.create()      @serialized_data = [        {          name: 'restaurant[name]',          value: 'Panjab'        },        {          name: 'restaurant[rating]',          value: '5'        },        {          name: 'restaurant[postcode]',          value: '123456'        }      ]      spyOn(@restaurant_form.$el, 'serializeArray').andReturn @serialized_data      afterEach ->      @server.restore()      it "should parse form data", ->      expect(@restaurant_form.parseFormData(@serialized_data)).toEqual validAttrs      it "should add a restaurant when form data is valid", ->      spyOn(@restaurant_form, 'parseFormData').andReturn validAttrs      @restaurant_form.save() # we mock the click by calling the method      expect(@restaurant_form.collection.length).toEqual 1      it "should not add a restaurant when form data is invalid", ->      spyOn(@restaurant_form, 'parseFormData').andReturn invalidAttrs      @restaurant_form.save()      expect(@restaurant_form.collection.length).toEqual 0      it "should send an ajax request to the server", ->      spyOn(@restaurant_form, 'parseFormData').andReturn validAttrs      @restaurant_form.save()      expect(@server.requests.length).toEqual 1      expect(@server.requests[0].method).toEqual('POST')      expect(@server.requests[0].requestBody).toEqual JSON.stringify(validAttrs)      it "should show validation errors when data is invalid", ->      spyOn(@restaurant_form, 'parseFormData').andReturn invalidAttrs      @restaurant_form.save()      expect($('.error', $(@invisible_form)).length).toEqual 1  

The pattern is exactly the same as the one we used in the previous spec: we instantiate a sinon server and check the requests array for a POST request with the valid attributes.

To implement this, we need to modify a line in views/restaurant_form.coffee:

  save: ->    data = @parseFormData(@$el.serializeArray())    new_restaurant = new Gourmet.Models.Restaurant data    errors =  new_restaurant.validate(new_restaurant.attributes)    if errors then @handleErrors(errors) else @collection.create new_restaurant  

Instead of simply adding the restaurant to the collection, we call the create method to trigger the server save.


Conclusion

If you have never worked with Backbone and Jasmine before, this is lot to digest, however the real benefit is the possibility to work effectively on testable pieces of functionality that follow predictable patterns. Here are some suggestions on how to improve from here:

  • Would it be possible to add a message to the validation errors?
  • How could we reset the form after adding a restaurant?
  • How could we edit a restaurant?
  • What if we need to paginate the table?

Try it out and let me know in the comments!




No hay comentarios:

Publicar un comentario