The Making of a Test-Driven Grocery List Application in JS: Part IV
2012 December 17th by todd anderson
This is the fourth installment in a series of building a Test-Driven Grocery List application using Jasmine and RequireJS. To learn more about the intent and general concept of the series please visit The Making of a Test-Driven Grocery List Application in JavaScript: Part I
—
Introduction
In the previous article we developed a new feature for the Grocery List application: Mark Off Item. In the process of doing so, or at least when we went from the tests to implementation, we added more responsibility to the list-controller as it pertained to individual view and model items.
In this article, we are going to refactor out that responsibility into its own list-item-controller that will be responsible for managing the relationship of a list item view to a single grocery-ls-item model.
Refactoring
As it stands , I think the design of the list-controller is fine: exposing an API to modify the list. It’s the internals that are starting to bug me. If the current behaviour and responsibilities were all that was needed, I suppose we could walk away and feel confident about our application as is. However, in forward thinking other operations that could involve list items – such as deletion – I feel the responsibilities of the list-controller will quickly outgrow its intent.
I suppose, some would argue, that if the responsibilities of the list-controller grow to more operations that are tightly coupled with singular pieces of data, then we could just write more tests to verify its soundness. Not entirely a bad argument – i mean that is what we are trying to justify in this series, essentially. But it is not a 1:1 correlation of more tests to better design. It is preferred to separate concerns as much as possible in order to properly test. The maintenance of complex tests can be a bigger burden than the maintenance of complex code – so much so that testing stops all together.
list-item-controller
Before we dive in to creating a list-item-controller, let’s take a look at what concerns within list-controller we want to move out. Looking from the 0.1.3 tagged list-controller, we mainly want to cut out the item view creation – so the following node declarations and any associated item creation and item management:
/script/controller/list-controller.js tagged at 0.1.3
itemFragment = '<li class="grocery-item" />',
editableItemFragment = '<li class="editable-grocery-item">' +
'<input id="editableItem" name="editableItem" ' +
'class="editable-item" placeholder="Enter item name...">' +
'</input>' +
'</li>'
These declarations and management of view will be moved to the list-item-controller. The UI and usability of a list item will be driven by the model and should provide an API that is a level of abstraction from the model for any outside parties (ie, the list-controller). As well, the model drives the internal state of edit-ability and marked-off-ed-ness (they’re words, alright!), and the list-item-controller will dispatch events related to its change of state. Lofty goals, but let’s see if we can’t address them.
a bit of uncertainty
We currently created specs for Add Item and Mark Off Item features. These were a little high-level in that they described features using the BDD syntax of Jasmine but did involve tests around how to interact with the list-controller API; so they do involve integration to some respect – we kind of ignore the whole UI and User Interaction aspect within the tests.
Now here is where I have an internal struggle with writing specs: should the tests we need for the list-item-controller be added to these specs? Or should a new spec focused on list-item-controller API and usability be born? I ask, because from outside-looking-in, the creation, editability and mark-off of an item will still be feasible through the list-controller and to any other third parties – that API provides a facade to modifying a grocery list. So, the current specs we have serve as a nice example of how to interact with the application from the outside… and if they are passing, we know that from higher-level things should* work
When we get down to the implementation of the list-item-controller, from a design perspective we know that a dependency will be introduced: list-controller will have n-number of list-item-controller instances, and will be responsible for the creation and maintenance of each list-item-controller. The state and grocery-ls-item model maintenance which we established in the previous articles as the responsibility of the list-controller will, as well, become the responsibility of the list-item-controller.
Knowing this, I start to feel that this is closer to testing the implementation and behaviour of a component, rather then one of the ’system’, and would push for its own spec. But I am very much open to ideas, so please leave a comment.
list-item-controller design
In my bit of uncertainty, I basically described the design of the list-item-controller. Now, we’ll flesh out its behaviour and API in a spec and eventually move it to its own AMD implementation. To start out slowly we’ll create a new spec suite and define the list-item-controller attributes:
/test/jasmine/spec/list-item-controller.spec.js
define(['jquery', 'script/model/grocery-ls-item'], function($, modelFactory) {
describe('list-item-controller', function() {
function createStateEvent(oldState, newState) {
var event = new $.Event('state-change');
event.oldState = oldState;
event.newState = newState;
return event;
}
var itemControllerFactory = {
create: function(node, model) {
var itemController = Object.create(Object.prototype);
(function(controller, stateEventCreator) {
var _state = 'normal';
Object.defineProperties(controller, {
"model": {
value: model,
writable: false,
enumerable: true
},
"parentView": {
value: node,
writable: false,
enumerable: true
},
"state": {
set: function(value) {
var event = stateEventCreator.call(this, this.state, value);
_state = value;
$(this).trigger(event);
},
get: function() {
return _state;
}
}
});
}(itemController, createStateEvent));
return itemController;
}
};
});
});
The object declared – itemControllerFactory – is a factory instance that will generate new instances of a list-item-controller. The factory pattern should be familiar to you if you look at the grocery-ls-item AMD we created in the second article in the series. When creating an instance of a list-item-controller, we have defined that a parent DOM node and a model should be passed in during creation, as can be seen in itemControllerFactory:create(). If we look at the defineProperties, we have also defined a few characteristics of the list-item-controller here. It has:
- Parent Node reference – DOM instance of which to modify view state.
- Model reference – Model of which the View is driven by.
- State – State of View within the DOM representing the Model.
We have designed the state property a little differently than you may have seen before, at least in this series. We declared the implicit getter/setters so as to keep track of state internally and dispatch an event of ’state-change’. By ‘internally‘, I mean I used an IIFE to enclose a ‘private‘ member storing state.
If you know of event-driven design – whether or not you are familiar with jQuery Events, which the state property employs – then this will look familiar. Essentially, we want to allow any client who wants to know about the change of state to a list-item-controller instance to be notified through an event of ’state-change’. This will become more apparent later on in development, but just keep in mind that it is the responsibility of the list-controller to maintain n-number of _list-item-controller_s; and part of that maintenance is being aware of each list-item-controller’s state – and since binding is not inherently available in JavaScript, and I don’t want to introduce any new libraries that could handle binding, listening on events is an easy way to go about tracking state. _You feel adventurous enough, we can build a binding mechanism on top of this event system… just make sure it’s got tests _
Tests
Before we begin creating the tests for the list-item-controller, you may be wondering where the feature stories and scenarios are. We could definitely define those, however – and of course, I can always be wrong – I feel those are more business-facing… well, stories at least. They are used to define some type of behaviour that is accepted as part of the software, with scenarios describing the various outcomes of a behaviour. Still valid for the case in hand of integrating the list-item-controller, but I tend to think this closer to testing on the implementation of behaviour. It’s a gray area to me, as well, if this all sounds confusing – I invite someone to step in and either clarify my understanding to set me straight.
Basically, our goal here is to verify the design and implementation of list-item-controller. If done properly this will pass, as well as the specs for the Add Item and Mark Off Item features we created in previous articles.
That said, let’s flesh out some tests that verify:
- Factory generates unique instances of list-item-controller
- Model is preserved and immutable on list-item-controller
- State is mutable through implicit getter/setters
- Change to state is dispatched
/test/jasmine/spec/list-item-controller.spec.js
describe('Grocery list-item-controller', function() {
var model,
newController,
async = new AsyncSpec(this);
beforeEach( function() {
model = modelFactory.create();
newController = itemControllerFactory.create(parentNode, model);
});
describe('list-item-controller factory creation', function() {
it('should return a new instance of list-item-controller', function() {
expect(newController).not.toBeUndefined();
});
it('should return unique instances of list-item-controllers', function() {
var nextController = itemControllerFactory.create(parentNode, model);
nextController.state = 'testing';
expect(nextController).not.toBe(newController);
expect(nextController.state).not.toBe(newController.state);
});
});
describe('new list-item-controller instance', function() {
it('should expose model provided in creation', function() {
expect(newController.model).not.toBeUndefined();
expect(newController.model).toBe(model);
});
it('should expose non-writable model', function() {
var newModel = modelFactory.create();
newController.model = newModel;
expect(newController.model).not.toBe(newModel);
expect(newController.model).toBe(model);
});
describe('list-item-controller notifies on state-change', function() {
async.it('should provide old and new state values on state-change', function(done) {
var previousState = newController.state,
newState = 'editable';
$(newController).on('state-change', function(event) {
$(newController).off('state-change');
expect(event.oldState).toBe(previousState);
expect(event.newState).toBe(newState);
expect(newController.state).toBe(newState);
done();
});
newController.state = newState;
});
});
});
afterEach( function() {
model = undefined;
newController = undefined;
});
});
The last spec declared, as you may notice, runs asynchronously in order to test the state notification:
async.it('should provide old and new state values on state-change', function(done) {
The AsyncSpec object comes from the jasmine.async library we’ve previously included in the specrunner page. By invoking the spec (it()) through an instance of AsyncSpec, the spec is suspended until either it fails or the done delegate method is invoked. Since notification of state change is event-based, we use it in the spec to verify that the list-item-controller does dispatch that event.
Add that spec to the list:
/test/jasmine/specrunner.html
require( ['spec/newitem.spec.js', 'spec/markitem.spec.js', 'spec/list-item-controller.spec.js'], function() {
var jasmineEnv = jasmine.getEnv(),
...
jasmineEnv.execute();
});
list-item-controller implementation
We’ve verified our design for the list-item-controller with passing tests, but we have yet to incorporate it into our system and offload the responsibilities (addressed earlier in this article) from the list-controller to it. Before we get there, however, let’s move the implementation of the list-item-controller (and the factory) out into its own AMD module. To start we’ll just move the itemControllerFactory declaration into a new file:
/script/controller/list-item-controller.js
define(['jquery'], function($) {
function createStateEvent(oldState, newState) {
var event = new $.Event('state-change');
event.oldState = oldState;
event.newState = newState;
return event;
}
return {
create: function(node, model) {
var itemController = Object.create(Object.prototype);
(function(controller, stateEventCreator) {
var _state;
Object.defineProperties(controller, {
"model": {
value: model,
writable: false,
enumerable: true
},
"parentView": {
value: node,
writable: false,
enumerable: true
},
"state": {
set: function(value) {
var event = stateEventCreator.call(this, this.state, value);
_state = value;
$(this).trigger(event);
},
get: function() {
return _state;
}
}
});
}(itemController, createStateEvent));
return itemController.init();
}
};
});
With that in place, we could modify the list-item-controller.spec.js file to include the list-item-controller dependency for testing:
/test/jasmine/spec/list-item-controller.spec.js
define(['jquery', 'script/model/grocery-ls-item', 'script/controller/list-item-controller'],
function($, modelFactory, itemControllerFactory) {
// moved to script/controller/list-item-controller.js
describe('Grocery list-item-controller', function() {
...
});
});
And our tests still pass! But… we want to move all the view and item management out of list-controller and have that handled by a list-item-controller. So let’s transfer over those view fragments and flesh out the list-item-controller object that is generated from the factory:
/script/controller/list-item-controller.js
define(['jquery'], function($) {
function createStateEvent(oldState, newState) {
var event = new $.Event('state-change');
event.oldState = oldState;
event.newState = newState;
return event;
}
var stateEnum = {
UNEDITABLE: 0,
EDITABLE: 1
},
uneditableItemFragment = '<p class="grocery-item" />',
editableItemFragment = '<p class="editable-grocery-item">' +
'<input name="editableItem" ' +
'class="editable-item" placeholder="Enter item name...">' +
'</input>' +
'</p>',
listItemController = {
$editableView: undefined,
$uneditableView: undefined,
init: function() {
this.$editableView = $(editableItemFragment);
this.$uneditableView = $(uneditableItemFragment);
// default to undeditable state.
this.state = stateEnum.UNEDITABLE;
return this;
}
};
return {
state: stateEnum,
create: function(node, model) {
var itemController = Object.create(listItemController);
(function(controller, stateEventCreator) {
var _state = 'normal';
Object.defineProperties(controller, {
"model": {
value: model,
writable: false,
enumerable: true
},
"parentView": {
value: node,
writable: false,
enumerable: true
},
"state": {
set: function(value) {
var event = stateEventCreator.call(this, this.state, value);
_state = value;
$(this).trigger(event);
},
get: function() {
return _state;
}
}
});
}(itemController, createStateEvent));
return itemController.init();
}
};
});
The init() method of a new list-item-controller is invoked upon creation and return in order to define the view references and default state. The fragment declarations have changed slightly as well – the list item wrappers have been removed. Basically, even though the name suggest that the view will reside in a list, we don’t want to tie the idea that they need to be li DOM elements since they also have no concept of what type of DOM element the parentView is.
As it stands with our current work from the previous article, the list-controller was responsible for listening on UI events of a list item – such as ‘blur’ on input and ‘click’. It is the intent to relive the list-controller of such responsibility so we’ll transfer that over, as well:
/script/controller/list-item-controller.js
listItemController = {
$editableView: undefined,
$uneditableView: undefined,
init: function() {
this.$editableView = $(editableItemFragment);
this.$uneditableView = $(uneditableItemFragment);
// view handlers.
this.$uneditableView.on('click', (function(controller) {
return function(event) {
var toggled = controller.$uneditableView.css('text-decoration') === 'line-through';
controller.model.marked = !toggled;
};
}(this)));
$('input', this.$editableView).on('blur', (function(controller) {
return function(event) {
controller.model.name = $(this).val();
controller.state = stateEnum.UNEDITABLE;
};
}(this)));
// default to undeditable state.
this.state = stateEnum.UNEDITABLE;
return this;
}
};
Again, we are using an IIFE, and in this case to pass in a reference to the controller instance. I could have easily defined a new variable like
var self = this;
but that always makes me cry a little inside. Anyway, so we are listening on click of the uneditable view item in order to update value of the marked property on the model and we have assigned a blur handler on the input of the editable view item that updates the value of the name property of the model and flips the state. Now we have to respond to those changes
/script/controller/list-item-controller.js
init: function() {
this.$editableView = $(editableItemFragment);
this.$uneditableView = $(uneditableItemFragment);
// view handlers.
this.$uneditableView.on('click', (function(controller) {
return function(event) {
var toggled = controller.$uneditableView.css('text-decoration') === 'line-through';
controller.model.marked = !toggled;
};
}(this)));
$('input', this.$editableView).on('blur', (function(controller) {
return function(event) {
controller.model.name = $(this).val();
controller.state = stateEnum.UNEDITABLE;
};
}(this)));
// state & model handlers.
$(this).on('state-change', (function(controller) {
return function(event) {
handleStateChange.call(null, controller, event);
};
}(this)));
$(this.model).on('property-change', (function(controller) {
return function(event) {
handlePropertyChange.call(null, controller, event);
};
}(this)));
// default to undeditable state.
this.state = stateEnum.UNEDITABLE;
return this;
}
The handleStateChange delegate is responsible for updating the view based on a change to state: either EDITABLE or UNEDITABLE from the stateEnum object:
function handleStateChange(controller, event) {
// remove state-based item.
if( typeof event.oldState !== 'undefined') {
if(event.oldState === stateEnum.UNEDITABLE) {
controller.$uneditableView.detach();
}
else if(event.oldState === stateEnum.EDITABLE) {
controller.$editableView.detach();
}
}
// append state-based item.
if(event.newState === stateEnum.UNEDITABLE) {
controller.parentView.append(controller.$uneditableView);
}
else if(event.newState === stateEnum.EDITABLE) {
var inputTimeout = setTimeout( function() {
clearTimeout(inputTimeout);
$('input', controller.$editableView).focus();
}, 100);
controller.parentView.append(controller.$editableView);
}
}
The handlePropertyChange delegate is responsible for updating the views based on the model property values:
function handlePropertyChange(controller, event) {
if(event.property === "name") {
// update view based on model change.
$('input', controller.$editableView).val(controller.model.name);
controller.$uneditableView.text(event.newValue);
}
else if(event.property === "marked") {
// update view based on model change.
controller.$uneditableView.css('text-decoration', (event.newValue) ? 'line-through' : 'none');
}
}
That pretty much shores up the implementation for the list-item-controller taking on the responsibilities of view creation and management based on state and model updates. But running the tests from this point will fail. The reason being a change to design on the model.
grocery-ls-item Modification
One particular change to design introduced in the implementation for list-item-controller is response to ‘property-change’ event from the model. In our work up to this point, the grocery-ls-item is a basic object; we’ll use the same paradigm that we implemented for notification of state for the grocery-ls-item in order to respond to changes on model properties. Although a simple change, we need to first test our design before modifying the code for the grocery-ls-item. Lets create a new spec file for our model:
/test/jasmine/spec/grocery-ls-item.spec.js
define(['jquery', 'script/model/grocery-ls-item'], function($, modelFactory) {
describe('Grocery grocery-ls-item model', function() {
var model,
name = 'grapes',
async = new AsyncSpec(this);
beforeEach( function() {
model = modelFactory.create();
model.name = name;
model.marked = false;
});
describe('grocery-ls-item factory model creation', function() {
it('should generate unique instances of model', function() {
var newModel = modelFactory.create();
expect(model).not.toBeUndefined();
expect(newModel).not.toBeUndefined();
expect(model).not.toBe(newModel);
});
async.it('should auto generate unique ids on models', function(done) {
var newModel,
creationTimeout;
// Offload creation because ids are generated based on time.
// This allows for timestamp to progess.
creationTimeout = setTimeout(function() {
clearTimeout(creationTimeout);
newModel = modelFactory.create();
expect(model.id).not.toBeUndefined();
expect(typeof model.id).toBe('number');
expect(model.id).not.toEqual(newModel.id);
done();
}, 100);
});
});
describe('grocery-ls-item properties', function() {
it('should contain an immutable id property, created at instantiation', function() {
var newID = 1234567;
model.id = newID;
expect(model.id).not.toEqual(newID);
});
});
describe('grocery-ls-item property change notification', function() {
async.it('should notify with \'property-change\' upon change to name property', function(done) {
var oldName = model.name,
newName = 'apples';
$(model).on('property-change', function(event) {
expect(event.property).toEqual('name');
expect(event.oldValue).toEqual(oldName);
expect(event.newValue).toEqual(newName);
$(model).off('property-change');
done();
});
model.name = newName;
});
async.it('should notify with \'property-change\' upon change to marked property', function(done) {
var oldValue = model.marked,
newValue = true;
$(model).on('property-change', function(event) {
expect(event.property).toEqual('marked');
expect(event.oldValue).toEqual(oldValue);
expect(event.newValue).toEqual(newValue);
$(model).off('property-change');
done();
});
model.marked = newValue;
});
});
afterEach( function() {
model = undefined;
});
});
});
We have a few suites there to verify:
- Model generation from factory produces unique items
- Model property immutability for auto-assigned IDs
- Event notification on property change
We add that to our spec runner:
require( ['spec/newitem.spec.js', 'spec/markitem.spec.js',
'spec/item-controller.spec.js', 'spec/grocery-ls-item.spec.js'], function() {
var jasmineEnv = jasmine.getEnv(),
...
jasmineEnv.execute();
});
… and it fails. Whoopee! It’s supposed to. Now we just take the knowledge we know of wiring up event notification on state of the list-item-controller, and apply it to property-change on the model:
/script/model/grocery-ls-item.js
define(['jquery'], function($) {
var propertyEvent = {
create: function(property, oldValue, newValue) {
var event = $.Event('property-change');
event.property = property;
event.oldValue = oldValue;
event.newValue = newValue;
return event;
}
},
properties = function(id) {
return {
"id": {
value: id,
writable: false,
enumerable: true
},
"name": {
enumerable: true,
set: function(value) {
var oldValue = this._name;
this._name = value;
$(this).trigger(propertyEvent.create('name', oldValue, value));
},
get: function() {
return this._name;
}
},
"marked": {
enumerable: true,
set: function(value) {
var oldValue = this._marked;
this._marked = value;
$(this).trigger(propertyEvent.create('marked', oldValue, value));
},
get: function() {
return this._marked;
}
}
};
};
return {
create: function() {
return Object.create(Object.prototype, properties(new Date().getTime()));
}
};
});
The modification to grocery-ls-item was perhaps our first introduction to writing failing tests due to a change in design prior to actually modifying the implementation. That feeling you’re feeling right now… that’s what makes this all worth it
Anyway… hold on to that feeling until the next post, because we are not done and it will go away quickly… just kidding
Tagged: 0.1.5 https://github.com/bustardcelly/grocery-ls/tree/0.1.5
Blinders
If you were to run the tests again… they still pass!
That is because we have not changed list-controller at all
Our list-item-controller.spec.js is happily oblivious to our recent additions and modifications, and the tests we wrote previously for the Add Item and Mark Off Item features still pass.
Before we just start chopping out and inserting code from list-controller, I want to go over the API and specs currently defined and see if they still hold water – meaning we might be able to cut some tests out. We might not. We might even add more… That’s what i plan to address in the next article of this series.
—
Link Dump
Reference
Test-Driven JavaScript Development by Christian Johansen
Introducing BDD by Dan North
Immediately-Invoked Function Expression (IIFE) by Ben Alman
RequireJS
AMD
Jasmine
Sinon
Jasmine.Async
Post Series
grocery-ls github repo
Part I – Introduction
Part II – Feature: Add Item
Part III – Feature: Mark-Off Item
Part IV – Feature: List-Item-Controller
Part V – Feature: List-Controller Refactoring
Part VI – Back to Passing
Part VII – Remove Item
Part VIII – Bug Fixing
Part IX – Persistence
Part X – It Lives!
Posted in AMD, JavaScript, RequireJS, grocery-ls, jasmine, unit-testing.