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.
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.
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
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
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:
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
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.