Test-Driven Development of AngularJS Applications
- 6. Imperative code
• GUIs are declarative
• HTML, CSS are declarative
• Front end code is mostly imperative
• Difficult to understand
• Maintenance nightmares
- 7. Lack of modularity
• Monolithic applications
• Rigid and interconnected code
• Difficult to test
• Forced to use hight level integration tests
• Large team issues
- 8. Testability
• Front end code is poorly tested
• Poor support from libraries
• jQuery
• Backbone.js
• In browser testing
• Lack of command line tools
- 11. node.js
• Platform
• JavaScript
var http = require('http');!
!
http.createServer(!
function (request, response) {!
response.writeHead(200, {'Content-Type': 'text/plain'});!
response.end('Hello Worldn');!
}!
).listen(8000);!
!
console.log('Server running at http://localhost:8000/');
• Google’s V8 JavaScript engine
• Created by Ryan Dahl
- 13. package.json
{
"name": "AngularDo",
"version": "1.0.0",
"dependencies": {
"angular": "~1.0.7",
"json3": "~3.2.4",
"jquery": "~1.9.1",
"bootstrap-sass": "~2.3.1",
"es5-shim": "~2.0.8",
"angular-resource": "~1.0.7",
"angular-cookies": "~1.0.7",
"angular-sanitize": "~1.0.7"
},
"devDependencies": {
"angular-mocks": "~1.0.7",
"angular-scenario": "~1.0.7"
}
}
- 20. Yo
create a new web app
• mkdir AngularApp && cd $_
• yo angular
• yo angular:controller
- 22. bower.json
{
"name": "AngularDo",
"version": "1.0.0",
"dependencies": {
"angular": "~1.0.7",
"json3": "~3.2.4",
"jquery": "~1.9.1",
"bootstrap-sass": "~2.3.1",
"es5-shim": "~2.0.8",
"angular-resource": "~1.0.7",
"angular-cookies": "~1.0.7",
"angular-sanitize": "~1.0.7"
},
"devDependencies": {
"angular-mocks": "~1.0.7",
"angular-scenario": "~1.0.7"
}
}
- 25. Jasmine Suites
describe("A suite", function() {
var flag;
!
beforeEach(function() {
flag = true;
});
!
it("contains spec with an expectation", function() {
expect(flag).toBe(true);
});
});
- 29. Features
• Display list of tasks
• Add a new task
• Mark task as done
• Add a new task with a priority
• Filter tasks by priority
• Search tasks
• Task counter
- 33. Install dependencies
• rvm install 2.0
• gem install compass
• brew install nodejs
• npm install -g bower
• npm install -g yo
• npm install -g generator-angular
• npm install -g karma
- 38. Rails RESTful back-end
• curl -L https://get.rvm.io | bash -s stable
• rvm install 2.0
• git clone git@github.com:dracco/AngularDoStore.git
• cd AngularDoStore
• bundle
• rails s
- 40. Angular front-end
• git clone git@github.com:dracco/AngularDo.git
• cd AngularDo
• npm install
• bower install
• grunt server
- 50. User story
As a user,
I should be able to see list of tasks,
so I can choose the next task
!
Scenario: Display list of tasks
When I navigate to the task list
Then I should see the list of tasks
- 55. TaskCtrl unit test
!
describe("TaskCtrl", function() {
it('should populate scope with list of tasks',
inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
$controller('TaskCtrl', { $scope: scope });
expect(scope.tasks.length).toEqual(3);
}));
});
- 62. Feature #1 Summary
• List of tasks (ng-repeat)
• Task list (TaskCtrl)
• e2e scenario
• TaskCtrl unit test
• No low level DOM manipulation (ng-repeat)
- 63. Feature #1 Summary
• LiveReload of the browser
• App code watcher
• Unit test watcher
• e2e scenario watcher
- 67. User Story
As a user,
I should be able to add a new task,
so I can update my list of tasks
!
Scenario: Add a valid new task
When I add a valid new task
Then I should see the task in the list
!
Scenario: Add an invalid new task
When I add an invalid new task
Then I should see an error message
- 68. e2e scenario
describe("Add a new task", function() {
describe("when the new task is valid", function() {
beforeEach(function() {
input('item.name').enter("New item");
element('button.js-add').click();
});
!
it("should add it to the list", function() {
expect(element('tr.task:last').text()).toMatch(/New item/);
expect(repeater('tr.task').count()).toBe(4);
});
!
it('should clear the new item box', function() {
expect(input('item.name').val()).toEqual('');
});
});
...
- 69. e2e scenario
describe("Add a new task", function() {
...
!
describe("when the new task is invalid", function() {
beforeEach(function() {
input('item.name').enter("");
element('button.js-add').click();
});
!
it("should leave the task list unchanged", function() {
expect(repeater('tr.item').count()).toBe(3);
});
!
it("should display an error message", function() {
expect(element('div.alert').count()).toBe(1);
});
});
});
- 77. TaskCtrl unit test
describe("add", function() {
var task;
!
it("should adds new task to task list", function() {
task = jasmine.createSpy("task");
scope.add(task);
expect(scope.tasks.length).toEqual(4);
});
});
- 84. Feature #2 Summary
• Dynamic list (ng-repeat)
• Validations (requires, ng-minlength)
• Disabled button (ng-disabled)
• Tests
- 88. User Story
As a user,
I should be able to mark tasks as done,
so I can keep track of completed work
!
Scenario: Mark task as done
When I mark a task as done
Then the task should be remove from the list
!
- 89. e2e scenario
describe("Mark task as done", function() {
it("should remove the task from the task list", function() {
element('button.js-done:last').click();
expect(repeater('tr.task').count()).toBe(2);
});
});
- 94. remove() unit test
!
describe("remove", function() {
it("should remove the task from task list", function() {
var task = jasmine.createSpy("task");
scope.remove(1, task);
expect(scope.tasks.length).toEqual(2);
});
});
- 105. User Story
As a user,
I should be able to set task priority,
so I can keep track of urgent tasks
!
Scenario: Add a task with priority
When I add task with priority
Then the task list should include priorities
!
- 112. Priority unit test
it("should adds new task to task list", function() {
task = {name: 'Task 4', priority: 'high'}
scope.add(task);
expect(scope.tasks.length).toEqual(4);
expect(scope.tasks[3].name).toEqual('Task 4');
expect(scope.tasks[3].priority).toEqual('high');
});
- 114. Add priorities
.controller('TaskCtrl', function ($scope) {
$scope.tasks = [
{name: 'Task 1', priority: 'high'},
{name: 'Task 2', priority: 'medium'},
{name: 'Task 3', priority: 'low'}
];
!
$scope.add = function(task) {
var newTask = new Object();
newTask.name = task.name;
newTask.priority = task.priority;
$scope.tasks.push(newTask);
};
!
...
});
- 123. User Story
As a user,
I should be filter tasks by priority,
so I can find hight priority tasks
!
Scenario: Priority filter
When I select ‘high’ priority filter
Then I should see only high priority tasks
!
- 124. e2e scenario
describe("Filter by priority", function() {
describe("when high priority is selected", function() {
it("should display only high priority tasks", function() {
element("a.priority:contains('high')").click();
expect(repeater('tr.task').count()).toBe(1);
});
});
!
describe("when high priority is selected", function() {
it("should display only medium priority tasks", function() {
element("a.priority:contains('medium')").click();
expect(repeater('tr.task').count()).toBe(1);
});
});
!
...
- 126. filter
task.priority == query.priority
<tr ng-repeat="task in tasks | filter:query)" ...>
<li ng-class="{'active': query.priority == ''}">
<a ng-init="query.priority = ''"
ng-click="query.priority = ''; $event.preventDefault()"...>
All
</a>
</li>
- 134. User Story
As a user,
I should be able to search tasks,
so I can find important tasks
!
Scenario: Search task
When I search for ‘Task 1’
Then I should see ‘Task 1’ in the list
!
- 135. e2e scenario
describe("Task search", function() {
it("should only display task that match the keyword", function() {
input("query.name").enter("Task 1");
expect(repeater('tr.task').count()).toBe(1);
expect(element('tr.task').text()).toMatch(/Task 1/);
});
});
- 137. filter:query
<input ng-init="query.name = ''" ng-model="query.name" ...>
!
!
!
!
<button ng-click="query.name =''" ...>Clear</button>
!
!
!
!
<tr ng-repeat="task in tasks | filter:query" class="task">
- 145. User Story
As a user,
I should be able to persist my tasks,
so I can access my task anywhere
!
Scenario: Persist tasks
When I add a new task
Then it should be persisted in the database
!
Scenario: Mark as task as done
When I mark a task as done
Then it should be removed from the database
!
- 146. $resource unit tests
!
it("should save the new task", function() {
scope.add(task);
expect($save).toHaveBeenCalled();
});
it("should remove new task from data store", function() {
scope.remove(1, task);
expect(task.$remove).toHaveBeenCalled();
});
- 148. $resource
angular.module('AngularDoApp')
.controller('TaskCtrl', function ($scope, Task, $resource) {
...
})
.factory('Task', ['$resource', function($resource){
return $resource('http://localhost:3000/:path/:id', {}, {
query: {method:'GET', params:{path:'tasks.json'}, isArray:true},
get: {method:'GET', params:{path:''}},
save: {method:'POST', params:{path:'tasks.json'}},
remove: {method:'DELETE', params:{path:'tasks'}}
});
}]);;
- 149. $save, $remove
$scope.add = function(task) {
var newTask = new Task(); // use to be new Object()
newTask.name = task.name;
newTask.priority = task.priority;
newTask.$save();
$scope.tasks.push(newTask);
};
!
$scope.remove = function(index, task) {
var id = task.url.replace("http://localhost:3000/tasks/", '');
task.$remove({id: id});
$scope.tasks.splice(index, 1);
};
- 156. User Story
As a user,
I should be see the number of tasks,
so I can estimate amount of outstanding work
!
Scenario: Task counter
When I navigate to home page
Then I should see the number of tasks
- 160. pluralize unit test
describe('pluralizeFilter', function() {
it('should return pluralized number of nouns',
inject(function(pluralizeFilter) {
expect(pluralizeFilter(0, "apple")).toBe('No apples');
expect(pluralizeFilter(1, "apple")).toBe('1 apple');
expect(pluralizeFilter(2, "apple")).toBe('2 apples');
}));
});