The Making of a Test-Driven Grocery List Application in JS: Part V
2012 December 31st by todd anderson
This is the fifth 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
The previous article was prefaced to be a refactoring effort to remove responsibilities of item management from the list-controller to instances of a list-item-controller. We designed the list-item-controller through specs, moved the factory to its own AMD and modified not only the grocery-ls-item model, but how the list-item-controller responded to changes of it.
All important and – in my view – much needed changes. However, we fell short on our goal. Sure, the tests still passed, but we modified nothing within the list-controller to reflect this transfer of responsibility. Part of the reason for this is my attempt at not overloading each article in this series with information. The other part is that I wanted a little time to stew over how I actually saw the revision of list-controller.
We still want to keep our feature specs of Add Item and Mark Item (and we have a few more features still to address), but I begin to question if it is really necessary to expose that functionality on the API of the list-controller. I am starting to see that those features are really part of a collection of grocery-ls-item models, and the controllers are just responders to changes on the collection and models themselves – the basis of MVC design.
Just as we implemented list-item-controller in the previous article with a design to respond to changes on a provided grocery-ls-item model and update its state accordingly, I propose so should we refactor the list-controller to respond to changes on a collection of grocery-ls-item models.
Collection
A generic collection is basically an object that provides a higher-level API to interact with an array of items. The API can provide a more readable way of adding and removing items on an underlying Array instance instead of using methods like push or splice – which are base level and don’t use a nomenclature that is best suited for the action taken – but more importantly, the API provides a facade to operations on an array of items from which it can internally have stricter rules over such actions.
There are various libraries and frameworks out there that support collections, and even different type of collections – ie, sets, linked lists, etc. As has been mentioned in previous posts, I am trying not to introduce new libraries into this series as it may add unnecessary noise to the task at hand. This also allows for us to keep the Collection implementation lean and customizable for the Grocery List application; we’re also going to support event notification from the collection, which is not inherent in the JavaScript Array object.
Tests
We will create the design of a Collection object piecemeal as we write the specs for it. To start, we know that it is to be a wrapper around an Array source:
/test/jasmine/spec/collection.spec.js
define( ['jquery'], function($) {
var collectionImpl = {
itemLength: function() {
return this.list.length;
}
},
collectionFactory = {
create: function(source) {
var instance = Object.create(collectionImpl, {
"list": {
value: Array.isArray(source) ? source : [],
writable: true,
enumerable: true
});
return instance;
}
};
describe('Collection', function() {
describe('collection factory instance creation', function() {
it('should create unique instances of collection from create()', function() {
var collectionOne = collectionFactory.create(),
collectionTwo = collectionFactory.create();
expect(collectionOne).not.toBe(collectionTwo);
});
it('should create an empty collection without source array provided', function() {
var collection = collectionFactory.create();
expect(collection).not.toBeUndefined();
expect(collection.itemLength()).toBe(0);
});
it('should create a collection from source array provided', function() {
var items = ['apples', 'oranges'],
collection = collectionFactory.create(items);
expect(collection.itemLength()).toBe(2);
});
});
});
});
We have defined two specs that involve testing the wrapped Array source for a collection. Upon creation, the array should be empty if no source array supplied or filled with items if source provided. Pretty straight forward.
Again, we are only concerned with the latest-and-greatest browsers, so you may notice the use of Array.isArray when the list property is defined within the check the source argument on the collectionFactory.create() call.
aside
We could get into a lengthy discussion about the design of collectionImpl, and in particular that the list property is publicly accessible – i mean, after all, are we not defeating the point of providing an API to manipulate the list if some developer could just come along and access it directly? A valid point, and one I struggle with often. We could – and I would recommend, at times – using the functional inheritance pattern to ‘privately’ hold the underlying list within the collection. For example:
var collectionImpl = (function(source) {
var list = source;
return {
itemLength: function() {
return list.length;
}
};
});
var instance = collectionImpl([]);
That would ‘conveniently‘ make the underlying array ‘private’, but there are an arguable list of pros and cons to this approach – the biggest con is the creation of a new object with new functions and new properties each call, rather then a shared inheritance. In essence, each new instance of collection created using this pattern could then modify, add and/or remove methods so that the API is not the same across all ‘instances’ of the collection – that’s a big con for me.
For our implementation, let’s just take it with a grain of salt that we don’t expect anyone to maliciously access the source array of the collection and use the API we are defining in our tests
end aside
Even though in the last spec, we aren’t concerned with the underlying list being strictly equal to the provided source, we should probably check that the list has the correct items and is in the correct order:
/test/jasmine/spec/collection.spec.js
var collectionImpl = {
itemLength: function() {
return this.list.length;
},
getItemAt: function( index ) {
if( index < 0 || (index > this.itemLength() - 1) ) {
return undefined;
}
return this.list[index];
}
}
The getItemAt() method first verifies that the supplied index within the range of the wrapped array and returns undefined if not or the item held in the array at the supplied index. We can ensure that method works properly by adding a couple tests to the spec we already have for verifying the source provided on creation:
/test/jasmine/spec/collection.spec.js
it('should create a collection from source array', function() {
var itemOne = 'apples',
itemTwo = 'oranges',
collection = collectionFactory.create([itemOne, itemTwo]);
expect(collection.itemLength()).toBe(2);
expect(collection.getItemAt(0)).toEqual(itemOne);
expect(collection.getItemAt(1)).toEqual(itemTwo);
});
Let’s go ahead and add some useful methods that will aid in adding and removing items to and from the collection:
/test/jasmine/spec/collection.spec.js
var collectionImpl = {
itemLength: function() {
return this.list.length;
},
addItem: function(item) {
this.list.push(item);
return item;
},
removeItem: function(item) {
var index = this.list.indexOf(item);
if( index > -1 ) {
this.list.splice(index, 1);
return item;
}
return undefined;
},
removeAll: function() {
this.list.length = [];
},
getItemAt: function( index ) {
if( index < 0 || (index > this.itemLength() - 1) ) {
return undefined;
}
return this.list[index];
}
}
We could add some specs for addItem() to ensure that the list does grow with each item appended to the end:
/test/jasmine/spec/collection.spec.js
describe('collection item addition', function() {
var collection;
beforeEach( function() {
collection = collectionFactory.create();
});
it('should append item to list from addItem()', function() {
var item = 'grapes';
collection.addItem(item);
expect(collection.itemLength()).toBe(1);
expect(collection.getItemAt(0)).toEqual(item);
});
it('should maintain order during multiple additions', function() {
var itemOne = 'grapes',
itemTwo = 'grapefruit';
collection.addItem(itemOne);
collection.addItem(itemTwo);
expect(collection.itemLength()).toBe(2);
expect(collection.getItemAt(1)).toEqual(itemTwo);
});
afterEach( function() {
collection = undefined;
});
});
… and throw in a spec suite for testing the removal API on the Collection:
/test/jasmine/spec/collection.spec.js
describe('collection item removal', function() {
var collection,
itemOne = 'pineapple',
itemTwo = 'pear';
beforeEach( function() {
collection = collectionFactory.create();
collection.addItem(itemOne);
});
it('should remove only specified item and report length of 0 from removeItem()', function() {
collection.removeItem(itemOne);
expect(collection.itemLength()).toBe(0);
});
it('should remove specified item from proper index', function() {
collection.addItem(itemTwo);
collection.removeItem(itemOne);
expect(collection.itemLength()).toBe(1);
expect(collection.getItemAt(0)).toEqual(itemTwo);
});
it('should retain items in collection if item provided to removeItem() is not found', function() {
collection.addItem(itemTwo);
collection.removeItem('watermelon');
expect(collection.itemLength()).toBe(2);
});
it('should empty the list on removeAll()', function() {
collection.addItem(itemTwo);
collection.removeAll();
expect(collection.itemLength()).toBe(0);
});
afterEach( function() {
collection = undefined;
});
});
With that, we have roughly designed and verified the API for our custom Collection object. Let’s add it to our list of specs to run:
/test/jasmine/specrunner.html
require( ['spec/newitem.spec.js', 'spec/markitem.spec.js',
'spec/item-controller.spec.js', 'spec/grocery-ls-item.spec.js',
'spec/collection.spec.js'], function() {
var jasmineEnv = jasmine.getEnv(),
...
jasmineEnv.execute();
});
Run it and all is green!
Whoa. Settle down… we’re not done yet
Collection Events
If we think back to why we even started down this path of creating a Collection object, we’ll remember that its role in the Grocery List application is to serve as the model for the list-controller. It is the intention to eventually modify the list-controller to not only offload the responsibility of managing the relationship and interaction between individual grocery-ls-item models to their views, but also to respond to changes on the collection of _grocery-ls-item_s accordingly. As such, we will incorporate event notification into our Collection object, from which the list-controller can assign response delegates to changes on the collection.
Using jQuery Event as we have previously in the development of this application throughout this series, we’ll create a factory method that returns an event object based provided arguments. If you are familiar with collections from the Flex SDK, you might notice something similar :
/test/jasmine/spec/collection.spec.js
function createEvent(kind, items) {
var event = new $.Event('collection-change');
event.kind = kind;
event.items = items;
return event;
}
Basically, any event dispatched from the collection object will be of the same type, and its operation can be differentiated from the kind property. The items affected upon the associated operation (kind) are provided in an array as some operations may involve multiple items.
With the API we have defined for the collection, there are three operations that can modify the the list of items:
- add
- remove
- reset
The methods associated with add and remove are pretty self-explanatory and we will consider removeAll() as a reset on the collection. With this in mind, let’s append a couple tests to our spec suites for these event notifications:
/test/jasmine/spec/collection.spec.js
describe('collection item addition', function() {
...
async.it('should notify on addition of item', function(done) {
var item = 'grapes';
$(collection).on('collection-change', function(event) {
expect(event.kind).toBe('add');
expect(event.items.length).toBe(1);
expect(event.items[0]).toEqual(item);
$(collection).off('collection-change');
done();
});
collection.addItem(item);
});
...
});
...
describe('collection item removal', function() {
...
async.it('should notify on removal of item', function(done) {
collection.addItem(itemTwo);
$(collection).on('collection-change', function(event) {
expect(event.kind).toBe('remove');
expect(event.items.length).toBe(1);
expect(event.items[0]).toEqual(itemOne);
$(collection).off('collection-change');
done();
});
collection.removeItem(itemOne);
});
async.it('should notify on reset of collection', function(done) {
$(collection).on('collection-change', function(event) {
expect(event.kind).toBe('reset');
expect(event.items.length).toBe(0);
$(collection).off('collection-change');
done();
});
collection.removeAll();
});
afterEach( function() {
collection = undefined;
});
...
});
If you were to run the tests now, you would see a couple execute then hang intermittently as it waits for the async tests to timeout… because we haven’t implemented the dispatching of events from the collection yet
/test/jasmine/spec/collection.spec.js
var collectionImpl = {
itemLength: function() {
return this.list.length;
},
addItem: function(item) {
this.list.push(item);
$(this).trigger(createEvent('add', [item]));
return item;
},
removeItem: function(item) {
var index = this.list.indexOf(item);
if( index > -1 ) {
this.list.splice(index, 1);
$(this).trigger(createEvent('remove', [item]));
return item;
}
return undefined;
},
removeAll: function() {
this.list.length = [];
$(this).trigger(createEvent('reset', this.list));
},
getItemAt: function( index ) {
if( index < 0 || (index > this.itemLength() - 1) ) {
return undefined;
}
return this.list[index];
}
}
Now, if you were to run the tests, all should be happy.
Collection Module Implementation
Just as we have done with the list-item-controller from the previous article, we are going to take the work we had done in implementing the collection object within our tests and move it to an AMD module. This way, if we ever get around to refactoring the list-controller, we’ll be able to utilize the collection module:
/script/collection/collection.js
define(['jquery'], function($) {
function createEvent(kind, items) {
var event = $.Event('collection-change');
event.kind = kind;
event.items = items;
return event;
}
var _collectionEventKind = {
ADD: 'add',
REMOVE: 'remove',
RESET: 'reset'
},
collection = {
itemLength: function() {
return this.list.length;
},
addItem: function(item) {
this.list.push(item);
$(this).trigger(createEvent(_collectionEventKind.ADD, [item]));
return item;
},
removeItem: function(item) {
var index = this.getItemIndex(item);
if( index > -1 ) {
this.list.splice(index, 1);
$(this).trigger(createEvent(_collectionEventKind.REMOVE, [item]));
return item;
}
return undefined;
},
removeAll: function() {
this.list.length = 0;
$(this).trigger(createEvent(_collectionEventKind.RESET, this.list));
},
getItemAt: function(index) {
if( index < 0 || (index > this.itemLength() - 1) ) {
return undefined;
}
return this.list[index];
},
getItemIndex: function(item) {
return this.list.indexOf(item);
},
contains: function(item) {
return this.getItemIndex(item) != -1;
}
};
return {
collectionEventKind: _collectionEventKind,
create: function(source) {
var instance = Object.create(collection);
Object.defineProperty(instance, "list", {
value: Array.isArray(source) ? source : [],
writable: true,
enumerable: true
});
return instance;
}
};
});
We have basically ripped the collection and factory out from our test implementation and dropped it into its own file, returning the event kind enumeration object and the create() factory method to generate new collections for this AMD module.
To verify that our Collection module works correctly, let’s replace the implementation in the test with this module reference and run our tests again:
/test/jasmine/spec/collection.spec.js
define( ['jquery', 'script/collection/collection'], function($, collectionFactory) {
describe('Collection', function() {
...
});
});
Tagged: 0.1.6 https://github.com/bustardcelly/grocery-ls/tree/0.1.6
list-controller Refactoring
Passing tests are great! But currently, they are lying to us. Well… not really, but we have gone about all these new additions to our application and have yet still to refactor list-controller to utilize them. However, 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. Let’s see…
newitem.spec
As we designed the list-controller from a previous article, it oversaw the state of list items, list item views, and sort of supported a quasi-state of ‘editability’. While this provided an API to create a new item, it was forced into exposing an editableItem that was then mutable based on other parts of its API – ie, editFocusedItem() and saveFocusedItem(). All well and good to support the feature requirements at the time, but we have now moved the item and item view management – as well as the editable state – to the latest list-item-controller as can be seen in the repo tagged at 0.1.5. As such, I feel not only the list-controller, itself, should change to reflect these modifications, but also its API.
We are going to stick with our feature request to be able to add a new item to the Grocery List application through the list-controller, but will revisit how that is done in accordance to the new functionality of both the list-item-controller and list-controller; mainly we want to keep in mind that both should be driven by their respective model: grocery-ls-item and collection.
Let’s first revisit the specs we defined for new item feature from the second post in this series:
// spec
—
Scenario 1: Item added to list
Given a user requests to add an item to the list
And has provided a name for the item
When she requests to save the item
Then the list has grown by one item
And the list contains the item appended at the end
—
// spec
—
Scenario 2: Item not added to list
Given the list has a single item
And a user requests to add an item to the list
And has not provided a name for the item
When she requests to save the item
Then the list has the same items as stored previously
And the list does not add an empty-named item
—
In looking at them now, I feel that the actual Add Item feature is hidden in the Givens. A slight oversight now that we have progressed – perhaps one at the time as well, but we were delivering to features using TDD, so I have no qualms with features and implementations revisited and revised as the functionality of the application is fleshed out. In any event, I feel like these feature specs are more for a Save Item story, especially seeing as a User can edit an existing item. We’ll tackle the Save Item specifications for a later post, for now I want to revise the specifications for the Add Item feature.
Tests
The original story does not change, but we want to ensure that a grocery-ls-item model is returned on the API to create a new item from list-controller . With such a drastic refactor to the functionality of list-controller, I tend to do my thinking and designing within the tests and move to implementation – just as I have done previously with other components. Let’s take a look at the change to newitem.spec.js as a whole and then discuss the specs singularly:
/test/jasmine/spec/feature.newitem.spec.js
define(['jquery', 'script/controller/list-controller', 'script/controller/list-item-controller',
'script/collection/collection', 'script/model/grocery-ls-item'],
function($, listController, itemControllerFactory, collectionFactory, modelFactory) {
describe('New item creation from listController.createNewItem()', function() {
var newModel,
newItemController,
listControllerStub,
$listView = $('<ul/>'),
itemCollection = collectionFactory.create();
beforeEach( function() {
var $itemView = $('<li>');
newModel = modelFactory.create();
newItemController = itemControllerFactory.create($itemView, newModel);
listControllerStub = sinon.stub(listController, 'createNewItem', function() {
listController.getItemList().addItem(newModel);
$itemView.appendTo($listView);
return newModel;
});
listController.getItemList = sinon.stub().returns(itemCollection);
listController.setView($listView);
});
it('should return newly created model', function() {
var newItem = listController.createNewItem();
// loosely (duck-ly) verifying grocery-ls-item type.
expect(newItem).toEqual(jasmine.any(Object));
expect(newItem.hasOwnProperty('name')).toBe(true);
expect(newItem.hasOwnProperty('id')).toBe(true);
expect(newItem.id).not.toBeUndefined();
});
it('should add newly created item to collection', function() {
var newItem = listController.createNewItem(),
itemList = listController.getItemList();
expect(itemList.itemLength()).not.toBe(0);
expect(itemList.getItemAt(itemList.itemLength()-1)).toEqual(newItem);
});
it('should add new item controller to view', function() {
listController.createNewItem();
expect($listView.children().length).toBe(1);
});
afterEach( function() {
$listView.empty();
newModel = undefined;
newItemController = undefined;
itemCollection.removeAll();
listController.createNewItem.restore();
});
});
});
First off, you may notice that we are pulling in, as dependencies, every component we have basically created up to this point. Then, within the beforeEach() of the spec suite, using SinonJS we stub out the redesign and addition(s) to the API of list-controller; we are redefining the functionality of createNewItem() (which currently exists on list-controller) to return a grocery-ls-item instance and stubbing the getItemList() method which will return the underlying collection model.
You may notice that i am using sinon.stub in two different ways:
listControllerStub = sinon.stub(listController, 'createNewItem', function() {
listController.getItemList = sinon.stub().returns(itemCollection);
The former allows for you to redefine the function invoked upon the public method – in this case ‘createNewItem‘. In order to properly define a stub in such a manner without being shown errors in executing the tests, the method must already be available on the object you are stubbing. The latter allows you to stub a method that is not currently on the object. As you may notice, the instantiation of the two different stubs are different in their assignment on the object being stubbed. The operations within these stubs shouldn’t be taken as set in stone – they may change once we get to implementation in list-controller – but they setup our expectations that will be verified in the specifications. The first of which ensures the return of a grocery-ls-item through property assertions:
/test/jasmine/spec/feature/newitem.spec.js
it('should return newly created model', function() {
var newItem = listController.createNewItem();
// loosely (duck-ly) verifying grocery-ls-item type.
expect(newItem).toEqual(jasmine.any(Object));
expect(newItem.hasOwnProperty('name')).toBe(true);
expect(newItem.hasOwnProperty('id')).toBe(true);
expect(newItem.id).not.toBeUndefined();
});
The second spec tests that the newly created item is added to the collection exposed on list-controller:
/test/jasmine/spec/feature/newitem.spec.js
it('should add newly created item to collection', function() {
var newItem = listController.createNewItem(),
itemList = listController.getItemList();
expect(itemList.itemLength()).not.toBe(0);
expect(itemList.getItemAt(itemList.itemLength()-1)).toEqual(newItem);
});
And the third spec… well, it starts to address some of the expectations a User may have when using the Grocery List application, something we really haven’t tested for as of yet – you have to start somewhere, though, right? We are testing that in addition to a new model added to the collection, there is an associated view in the UI (or at least presumably):
/test/jasmine/spec/feature/newitem.spec.js
it('should add new item controller to view', function() {
listController.createNewItem();
expect($listView.children().length).toBe(1);
});
I think we will start to see more of these type of tests as we start fleshing out the features more.
Actually, I have started taking steps in moving the separation of integration tests from feature tests, if you haven’t already noticed the update to the location of newitem.spec.js. In my mind, the difference is between what I consider tests of how a component behaves itself (and with others) and test which describe the actual use of the application, respectively.
Run the tests just as you have before with the updates to the feature spec locations:
/test/jasmine/specrunner.html
require( ['spec/feature/additem.spec.js', 'spec/feature/markitem.spec.js',
'spec/item-controller.spec.js', 'spec/grocery-ls-item.spec.js',
'spec/collection.spec.js'], function() {
var jasmineEnv = jasmine.getEnv(),
...
jasmineEnv.execute();
});
And all is green!
Tagged 0.1.7 – https://github.com/bustardcelly/grocery-ls/tree/0.1.7
list-controller Revisted
It’s great that the tests still pass, but we have yet to still modify list-controller. I can’t put it off any longer. If you have put up with the last couple posts and promises to get somewhere, you are very kind. The wait is over! … but it is also just the tip of the iceberg. sorry to be a downer
In looking at the API on list-controller we stubbed out from the beforeEach() of the newitem.spec and the expectations of its functionality verified in the specs, we are basically boiling down the responsibilities of the list-controller to:
- Create a new item
- Add item views to a provided element
- Manage and respond to changes on a collection of grocery-ls-item
As a start, we can include the new dependencies and refactor the list-controller component to support these requirements:
/script/controller/list-controller.js
define(['jquery', 'script/controller/list-item-controller', 'script/collection/collection', 'script/model/grocery-ls-item'],
function($, itemControllerFactory, collectionFactory, modelFactory) {
var collection = collectionFactory.create(),
listController = {
$view: undefined,
getItemList: function() {
return collection;
},
createNewItem: function() {
var model = modelFactory.create();
collection.addItem(model);
return model;
},
setView: function(view) {
this.$view = (view instanceof $) ? view : $(view);
}
};
return listController;
});
Differing from the stub created for createNewItem() in the newitem.spec, the list-controller is only concerned with updating the collection model here. This is because the change to the collection will drive updates to the UI and we don’t want to have the UI operations within the createNewItem() method – that will basically create a doubling-up of efforts. It will be the responsibility of this module to observe changes to the collection and update state. That is done by adding an event handler to collection-change and operating accordingly based on event kind:
/script/controller/list-controller.js
define(['jquery', 'script/controller/list-item-controller', 'script/collection/collection', 'script/model/grocery-ls-item'],
function($, itemControllerFactory, collectionFactory, modelFactory) {
var collection = collectionFactory.create(),
listController = {
$view: undefined,
getItemList: function() {
return collection;
},
createNewItem: function() {
var model = modelFactory.create();
collection.addItem(model);
return model;
},
setView: function(view) {
this.$view = (view instanceof $) ? view : $(view);
}
};
(function assignCollectionHandlers($collection) {
var EventKindEnum = collectionFactory.collectionEventKind;
$collection.on('collection-change', function(event) {
switch( event.kind ) {
case EventKindEnum.ADD:
var model = event.items.shift(),
$itemView = $('<li>'),
itemController = itemControllerFactory.create($itemView, model);
$itemView.appendTo(listController.$view);
itemController.state = itemControllerFactory.state.EDITABLE;
break;
case EventKindEnum.REMOVE:
break;
case EventKindEnum.RESET:
break;
}
});
}($(collection)));
return listController;
});
We are calling an IIFE with the jQuery wrapped collection object and assigning an event handler to collection-change on the collection. Depending on the kind of collection-change event that has occurred, defined clauses with specified operations are entered – for the purposes of the current task and tests at hand, that is only the ADD event.
In the EventKindEnum.ADD switch..case you will see the UI modification, with the addition of the list item view and the editability state of its associated list-item-controller set to allow the User to edit the new item.
Now, with the implementation of the Add Item feature moved to list-controller, we can clean up our newitem.spec.js:
/test/jasmine/spec/feature/newitem.spec.js
define(['jquery', 'script/controller/list-controller'],
function($, listController) {
describe('New item creation from listController.createNewItem()', function() {
var $listView = $('<ul/>');
beforeEach( function() {
listController.setView($listView);
});
it('should return newly created model', function() {
var newItem = listController.createNewItem();
// loosely (duck-ly) verifying grocery-ls-item type.
expect(newItem).toEqual(jasmine.any(Object));
expect(newItem.hasOwnProperty('name')).toBe(true);
expect(newItem.hasOwnProperty('id')).toBe(true);
expect(newItem.id).not.toBeUndefined();
});
it('should add newly created item to collection', function() {
var newItem = listController.createNewItem(),
itemList = listController.getItemList();
expect(itemList.itemLength()).not.toBe(0);
expect(itemList.getItemAt(itemList.itemLength()-1)).toEqual(newItem);
});
it('should add new item controller to view', function() {
listController.createNewItem();
expect($listView.children().length).toBe(1);
});
afterEach( function() {
$listView.empty();
});
});
});
And we’ll modify the main application module to reflect the change to the list-controller now only governing over a list DOM element and not to manage events from the add button on the DOM:
/script/grocery-ls.js
(function(window, require) {
require.config({
baseUrl: ".",
paths: {
"lib": "./lib",
"script": "./script",
"jquery": "./lib/jquery-1.8.3.min"
}
});
require( ['jquery', 'script/controller/list-controller', 'script/collection/collection'],
function($, listController, collectionFactory) {
listController.setView($('section.groceries ul'));
$('section.groceries #add-item-button').on('click', function(event) {
listController.createNewItem();
});
});
}(window, requirejs));
Run the tests… and it will fail! Yay!
wait, what?!
Actually, most will pass – including the newitem.spec tests. It is the markitem.spec tests that will fail. We have modified the list-controller to accomodate the changes to the Add Item feature, but have not addressed the Mark Off Item feature in our refactoring.
However, run the application and it will be just as usable as it was before – no change will be perceived by the end-user. The only thing that will change is now I have a nagging feeling knowing my tests are failing. Some naysayers may interject here and half-heartedly tell me to get rid of the tests, then. To them I say, ‘pfffft‘
I hate to see the tests in such a state, but this post is rather long as it is. Also, I like to joke that sometimes having failing tests to look at first thing in the morning is the best way to pick up from where you left off. I promise we’ll get these to go green in the next article of this series, and invite you to get them to pass if you are for it.
Tagged 0.1.8 – https://github.com/bustardcelly/grocery-ls/tree/0.1.8
Conclusion
We finally got around to refactoring the list-controller to relieve it of item model management and state. In the course of doing so, we created a Collection object that will serve as the model for the list-controller.
This post was a lengthy one, and I appreciate you sticking through my yackity-yack. I think we are in fine shape now to approach previously defined and new features, get our tests passing again, and finalize the Grocery List application… in as far as first iteration deliverables go
‘Til next time…
—
Link Dump
Reference
Test-Driven JavaScript Development by Christian Johansen
Introducing BDD by Dan North
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.