The Code of No-Code

Sep 26 2013

A simple example to illustrate the point:

  • You just made some changes in your Rails application
  • All your Rails specs pass
  • All your Javascript specs pass

However you know this is wrong! You've properly written your unit tests such that there is no database communication between the two, thus there is no way for your Backbone models to know about the changes on the Rails side. Until you fire up your Rails server and everything is broken.

How do we bridge this gap? Let's look at a simple example. It's a bit contrived, but it's just to illustrate the problem. Assume you have the following Rails model (not shown), Fabricator (using the Fabrication gem), and Backbone model (and Jasmine specs).

spec/fabricators/person_fabricator.rb:

Fabricator(:person) do
  name 'Brett Pontarelli'
end

javascripts/app/models/person.js.coffee:

class Person extends Backbone.Model
  defaults:
    name: ''

  isValid: ->
    _.isString(@name) && @name.length > 0

javascripts/spec/models/person_spec.js.coffee:

describe 'Person', ->
  it 'should have a name', ->
    person = new Person()
    expect(person.isValid()).toBe(false)
    person.set('name', 'Brett Pontarelli')
    expect(person.isValid()).toBe(true)

Now we change our Rails model to have a first and last name. Then update our Fabricator and get our specs passing.

Fabricator(:person) do
  first_name 'Brett'
  last_name 'Pontarelli'
end

At this point it would be nice if our Javascript specs failed, alerting us to change our Backbone model, but there's no connection between them (Rails and Backbone) so all our specs still pass. What we need to do is create a link that is not database dependent, but injects our Rails fabricators into our Javascript. To do this I wrote a Fabrication class for Jaff that can be used very similarly to the Rails fabrication gem.

First, we back out our Rails model changes and then refactor our front end specs using our new Jaff fabrication functions. For example you might make Fabricator and Fabricate globals (don't freak out it's only for convenience and they are isolated to our Jasmine specs).

javascripts/spec.js.coffee:

#= require_self
#= require_tree ./spec

Fabrication = new Jaff.Fabrication()
window.fabricate = (className, fabricationFunction) ->
  Fabrication.fabricate(className, fabricationFunction)
window.fabricator = (className, overrides) ->
  Fabrication.fabricator(className, overrides)

First we define the fabricator. Notice the .erb extension which let's us inject our Rails fabricator into our Javascript. It's necessary to use attributes_for, to_json, and gsub to build a valid Javascript object.

javascripts/spec/fabricators/person_fabricator.js.coffee.erb:

person = <%= Fabricate.attributes_for(:person).to_json.gsub('\\','') %>

fabricator Person, ->
  _.extend({}, person, id: _.uniqueId())

Which after going through Rails asset pipeline (remember we rolled back our Rails changes) will look like:

(function() {
  person = {
    "name": "Brett"
  }
  return fabricator(Person, function() {
    return _.extend({}, person, {
      id: _.uniqueId()
    });
  });
}).call(this);

Next we rewrite our spec and get all our tests passing:

javascripts/spec/models/person_spec.js.coffee:

describe 'Person', ->
  it 'should have a name', ->
    person = fabricate(Person)
    expect(person.isValid()).toBe(true)

  it 'should have a name whose length > 0)', ->
    person = fabricate(Person, name: '')
    expect(person.isValid()).toBe(false)

What happens when we roll our first and last name changes into Rails? Well, the person_fabricator.js after it's gone through the Rails asset pipeline will now look like:

(function() {
  person = {
    "first_name": "Brett",
    "last_name": "Pontarelli"
  }
  return fabricator(Person, function() {
    return _.extend({}, person, {
      id: _.uniqueId()
    });
  });
}).call(this);

and our first test is going to fail due to the fact that our fabricated Person will not have a name and our second test will pass, because we are specifically setting name: '' to an invalid (according to the current Backbone model) value. The great thing about this is that after making our Rails changes our front end specs will immediately fail forcing us to get them in sync with the new changes.