Test-driven Backbone.js - Part One
January 23, 2013
In this series of posts I’m going to walk through some practical details on how we can develop Backbone.js applications in a test-driven manner. Developing this way leads to better structured applications, as we’ll discover.
Setup
We’ll be using backbone and underscore to build a single page app which simulates a card wall. We’ll be writing our app and our tests using coffeescript. If there’s enough interest I’d be happy to set up a JavaScript translation of the code snippets - please let me know. We’ll be using Jasmine as our test runner and we’ll be using sinon.js to create test doubles (mocks and stubs). We’ll also use a few other small utilities and plugins as we go.
Part one - test driven models
Let’s start off simple and test-drive a Backbone model which will represent a card on the wall. Test-driven means writing the test before the production code, so we’ll start off by writing a test for the model, before the model even exists. I like to start off a new test file with a really dumb test that just drives out the initial setup of the class:
Pretty much the simplest test I can think of: check that Card
has been defined somewhere. Of course when we run this test it will fail, because we haven’t defined that model yet. Let’s do that.
OK, with that done our first test is passing.
I know, I know. We didn’t define Card
to be a backbone model. I’m being a bit dogmatic here for a moment and following the test-driven tenent of doing the Simplest Possible Thing to get the test passing. In this case defining Card
as a string is a simple thing to do, so that’s what I did. Using the keyword ‘slime’ is a trick I picked up from Gary Bernhardt to indicate that this is obviously not finished code.
So, we have a slimed Card definition which passes the test. Our next step is to write a test which drives us to remove that slime and make Card
a real Backbone.Model.
This new test verifies that Card instances ‘quack like a Model’. Since JavaScript doesn’t really have a strong notion of type I follow a duck-typing approach here and just verify that the Card instance implements methods that I’d expect a Backbone Model to have. This is good enough to drive us towards a more sensible initial implementation of Card
:
Note that I’m still sticking to the Simplest Possible Thing tenent. Our test doesn’t expect Card
to have any additional functionality beyond the vanilla functionality it gets as a Backbone.Model, so I don’t bother to use Backbone.Model.extend
. When I’m driving out code this way it’s not unusual that I never have to go beyond what I thought at the time was a placeholder implementation. I wouldn’t be surprised if that holds true here and that we end up never needing to extend Card
beyond this vanilla implementation.
A Card Wall
Now that we have a Card
defined, let’s look at building a Wall of cards. There’s a temptation here to think ‘Oh, a wall of cards is like a collection of cards; let’s define it as a Backbone.Collection’. That’s a bad idea here. A card wall does contain a collection of cards, but it will likely also have other attributes. It might have a title, and an owner maybe. I’ve found that with Backbone if you’re modeling an entity in your system that’s more than just purely a collection of other things then it’s best to represent that entity as a Backbone.Model which contains a Backbone.Collection as an attribute, rather than modelling that entity as a Backbone.Collection with additional custom properties. If you use custom properties rather than the attributes of a Backbone.Model then you lose all the nice functionality that Backbone gives your model’s attributes (e.g. serialization, change events).
Given that, let’s test-drive a CardWall
which contains a collection of Cards
. Again we’ll start off really simply:
This test will fail because we haven’t defined CardWall
anywhere. Let’s get it to pass:
That gets our tests back to green.
Note that I’ve drifting a little from my previous Simplest Possible Thing dogmatism. I know that I’m going to be making a Backbone.Model and I’m gaining a bit more confidence in my TDD flow, so I’m going to start taking slightly bigger TDD steps and define CardWall
as a Backbone.Model even though the test isn’t strictly requiring that.
Taking bigger TDD steps like this is OK, as long as we’re always ready to slow down and taking smaller steps if we start feeling uncomfortable with our code. A good rule of thumb is that if you’re stuck in the Red part of your TDD cycle for more than a few minutes then you’re probably taking steps which are too big and should dial it back. As an aside, Kent Beck has a very interesting discussion around this in a presentation he did a few years ago on design principles - it’s well worth a watch. And hey, if Kent bends the TDD ‘rules’ sometimes then we’re allowed to too.
OK, now let’s drive out a cards
collection on the CardWall
model as we discussed earlier.
We’re checking that our card wall has a cards
attribute, and then we’re checking that the cards
attribute ‘quacks like’ a Backbone.Collection by checking that is has a models
property.
Now let’s get this test passing:
This is a pretty big step, but it gets our tests back to green. We’ve defined a Cards
collection, and we’ve extended our CardWall
model to have an instance of that collection as a cards
attribute by default.
Pause to refactor
Our tests are green again, but instead of moving on to our next test I’m going to take a moment to refactor. It’s really easy to forget the Refactor part of the Red-Green-Refactor cycle but focusing on Refactor is crucial if you want TDD to drive you towards cleaner code. Without refactoring frequently you will end up with a codebase which is functional but not maintainable. Over time changes will become more and more expensive, and tests will take longer and longer to write and get passing.
So, let’s refactor! I’m going to DRY up our card wall tests a little bit:
All I’ve done here is pull out a shared cardWall
variable and set it up in a beforeEach
function. A small change, but it reduces some duplication from the tests and makes them a little bit easier to read. Small refactorings like this sometimes seem unnecessary in the moment but applying refactorings like this continuously over time they will do amazing things to your code. Imagine living in a codebase which always feels like a greenfield project.
What’s next? Let’s give CardWall
a title
attribute, and have it default to something sensible:
And let’s get that test to pass:
And we’re back to green.
Adding cards to a card wall
We are going to want to add Card
instances to our CardWall
. A client of CardWall
could do that with the existing implementation by doing something like myCardWall.get('cards').add( cardProperties )
, but that’s not a nice approach. It’s ugly to read and it’s a Law of Demeter violation which exposes the internals of CardWall
. Let’s expose a nice helper method on CardWall
that hides those details and makes the client’s life easier. Here’s a test describing what we want the method to do:
We expect to be able to call an addCard
method which will add a card to the CardWall’s cards
collection with the appropriate attributes. We can get that test passing with a one-line method:
Testing events
Clients of our CardWall
model are going to want to know when cards are added to our wall. By default a Backbone model only fires a change event when one of its attributes is re-assigned, not when one of its attributes changes internally. That means that a client which has subscribed to changes on a CardWall
instance won’t be notified when its cards
collection changes. Let’s confirm that problem via a test:
We’re creating a Sinon spy function and registering that function with our cardWall
instance as an event handler for the change:cards
event. So whenever cardWall
fires a change:cards
event that spy function will be called. We then add a card to cardWall
using the addCard
method we created previously and then check whether our spy has been called. If the spy was called then that change:cards
event has indeed been published. If it wasn’t called then we know that the change:cards
event isn’t firing when we add a card via addCard
.
As expected, this test fails because a Model doesn’t automatically notify subscribers of changes inside its attributes. However we can get the test to pass pretty simply:
We’ve added a trigger
call at the end of our addCard
method. This will fire off the expected event whenever we add a card. With that change our tests are back to green, and we’re at a good point to wrap up this installment.
What have we covered
So what did we cover in this installment?
We’ve learned the basics of how to test-drive a Backbone model. We’ve learned that we should strive to get tests passing by doing the Simplest Possible Thing, but that we don’t need to be to dogmatic about that. We’ve discovered that a Backbone.Collection is only approriate for modelling a pure collection of items. We’ve seen that it’s better to encapsulate access to internal attributes by using helpful functions like addCard
. Finally we’ve seen how to use Sinon spies to test basic Backbone event publication.
No DOM, no jQuery
One final thing to note before we wrap up - we have not made any reference to the DOM in these tests. In fact, when developing this code I ran my tests within node.js with not a DOM implementation in sight. This is a very important point, and a key property of a well-structured Backbone application - Backbone views should be the only thing referencing the DOM (and therefore the only thing using JQuery’s almighty $).
The flip side of keeping the DOM and jQuery contained without our Views is that we should always strive to keep our Backbone Views as skinny and logic-free as possible. Because Views interact with the DOM they are particularly tricky to test, and our tests are slower to run because we have to run them in a browser. We want to keep our views as simple as possible so that we don’t have to write too many of those tricky tests. Instead we want to push logic into other parts of our application where it can be easily tested in isolation.
In the next installment we’ll dive into testing Backbone Views and their interaction with the DOM. I’ll also go into more details on what a View’s responsibilities should be.