SlideShare a Scribd company logo
Test Driven AngularJS
Andy Pliszka	


!
!

@AntiTyping	

AntiTyping.com	

github.com/dracco
Problems
jQuery
• Low-level DOM modification
• Inserting data into DOM
• Extracting data from DOM
• Code duplication
Boilerplate code
• Copy and paste
• jQuery DOM manipulation
• Backbone.js views
• Event handlers
Lack of Structure
• Rails folder structure
• Django folder structure
• Running tests
Imperative code
• GUIs are declarative
• HTML, CSS are declarative
• Front end code is mostly imperative
• Difficult to understand
• Maintenance nightmares
Lack of modularity
• Monolithic applications
• Rigid and interconnected code
• Difficult to test
• Forced to use hight level integration tests
• Large team issues
Testability
• Front end code is poorly tested
• Poor support from libraries
• jQuery
• Backbone.js
• In browser testing
• Lack of command line tools
Problem
Summary
Toolset
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
npm
• Official package manager for Node.js
• npm search
• npm install
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"
}
}
YOEMAN
Automate
• Repetitive tasks
• Tests
• Compilation of assets
Create
• Bootstrap the app
• Folder structure
• Generators
Development
• Watch files
• Recompile (Sass, CoffeeScript)
• Reload browser
Deploy
• Testing
• Linting and compilation
• Concatenation and minification
• Image optimization
• Versioning
Installation
• brew install nodejs
• npm install -g yo
• npm install -g generator-angular
Yo
create a new web app

• mkdir AngularApp && cd $_
• yo angular
• yo angular:controller
Bower
manage dependencies

• bower search
• bower install
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"
}
}
Grunt
preview, test, build

• grunt server
• grunt test
• grunt build
Jasmine
• Behavior-driven development framework
• Specs for your JavaScript code
• Write expectations
• Uses matchers
Jasmine Suites
describe("A suite", function() {
var flag;
!
beforeEach(function() {
flag = true;
});
!
it("contains spec with an expectation", function() {
expect(flag).toBe(true);
});
});
Jasmine Expectations
describe("A suite", function() {
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
});
Jasmine Matchers
expect(a).toBe(b);
expect(a).not.toBe(null);
expect(a).toEqual(12);
expect(null).toBeNull();
!
expect(message).toMatch(/bar/);
!
expect(a.foo).toBeDefined();
expect(a.bar).toBeUndefined();
!
expect(foo).toBeTruthy();
expect(a).toBeFalsy();
!
expect(['foo', 'bar', 'baz']).toContain('bar');
!
expect(bar).toThrow();
Demo
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
Feature UI
Tracker
Setup
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
Project setup
• mkdir AngularDo
• cd AngularDo
• yo angular AngularDo
yo angular AngularDo
AngularDo app
grunt server
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
rails s
Angular front-end
• git clone git@github.com:dracco/AngularDo.git
• cd AngularDo
• npm install
• bower install
• grunt server
Test-Driven Development of AngularJS Applications
Angular front-end
Project structure
./run-e2e-tests.sh
./run-unit-tests.sh
Dev setup
• grunt server
• rails s
• ./run-unit-tests.sh
• ./run-e2e-tests.sh
Feature #1
List of tasks
git checkout -f feature_1_step_0
List of tasks
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
Test-Driven Development of AngularJS Applications
e2e scenario
describe("Task List", function() {
it('should display list of tasks', function() {
expect(repeater('tr.item').count()).toBe(3);
});
});
Red scenario
ng-repeat
<tbody>
<tr ng-repeat="task in tasks" class="task">
<td>{{$index + 1}}</td>
<td>{{task.name}}</td>
</tr>
</tbody>
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);
}));
});
Red unit test
TaskCtrl
'use strict';
!
angular.module('AngularDoApp')
.controller('TaskCtrl', function ($scope) {
$scope.tasks = [
{name: 'Task 1'},
{name: 'Task 2'},
{name: 'Task 3'},
];
});

<div class="row" ng-controller="TaskCtrl">
Green TaskCtrl test
Green e2e scenario
List of tasks
All test are green
Feature #1 Summary
• List of tasks (ng-repeat)
• Task list (TaskCtrl)
• e2e scenario
• TaskCtrl unit test
• No low level DOM manipulation (ng-repeat)
Feature #1 Summary
• LiveReload of the browser
• App code watcher
• Unit test watcher
• e2e scenario watcher
Feature #2
Add a new task
git checkout -f feature_2_step_0
Feature UI
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
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('');
});
});
...
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);
});
});
});
Red scenario
ng-model

<input name="name"
ng-model="task.name" required ng-minlength="3" ...>
ng-click
<button ng-click="add(task); task.name = '';"
ng-disabled="form.$invalid" ...>Add</button>
ng-show
<div ng-show="form.name.$dirty &&
form.name.$invalid &&
form.name.$error.minlength" ...>
Task name should be at least 3 characters long.
</div>
Test-Driven Development of AngularJS Applications
Error message
Red scenario
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);
});
});
Red unit test
TaskCtrl
angular.module('AngularDoApp')
.controller('TaskCtrl', function ($scope) {
$scope.tasks = [
{name: 'Task 1'},
{name: 'Task 2'},
{name: 'Task 3'},
!
];
!
$scope.add = function(task) {
var newTask = new Object();
newTask.name = task.name;
$scope.tasks.push(newTask);
};
});
Green unit test
Green e2e scenario
Test-Driven Development of AngularJS Applications
All test are green
Feature #2 Summary
• Dynamic list (ng-repeat)
• Validations (requires, ng-minlength)
• Disabled button (ng-disabled)
• Tests
Feature #3
Mark task as done
git checkout -f feature_3_step_0
Feature UI
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
!
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);
});
});
Red scenario
ng-click
<td>
<button ng-click="remove($index, task)" class="js-done">
Done
</button>
</td>
Test-Driven Development of AngularJS Applications
Red scenario
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);
});
});
Red unit test
remove()
angular.module('AngularDoApp')
.controller('TaskCtrl', function ($scope) {
...
!
$scope.remove = function(index, task) {
$scope.tasks.splice(index, 1);
};
});
Green unit test
Green e2e scenario
Test-Driven Development of AngularJS Applications
All test are green
Feature #3 Summary
• e2e scenario
• TaskCtrl unit test
• Click handler (ng-click)
Feature #4
Add task with priority
git checkout -f feature_4_step_0
Feature UI
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
!
e2e scenario

!
it("should set priority", function() {
expect(element("span.priority:last").text()).toMatch(/medium/);
});
Red scenario
ng-init
<select ng-init="task.priority = 'high'" ng-model="task.priority">
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
Test-Driven Development of AngularJS Applications
Red scenario
{{task.priority}}
<tr ng-repeat="task in tasks" class="task">
<td>{{$index + 1}}</td>
<td>
{{task.name}}
<span class="priority label">{{task.priority}}</span>
</td>
...
</tr>
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');
});
Red unit test
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);
};
!
...
});
Green unit test
Green e2e scenario
Test-Driven Development of AngularJS Applications
All test are green
Feature #5 Complete
Feature #5
Priority filter
git checkout -f feature_5_step_0
Feature UI
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
!
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);
});
});
!
...
Red scenario
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>
Green e2e scenario
Test-Driven Development of AngularJS Applications
All test are green
Feature #5 Complete
Feature #6
Search tasks
git checkout -f feature_6_step_0
Feature UI
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
!
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/);
});
});
Red scenario
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">
Test-Driven Development of AngularJS Applications
Green e2e scenario
Test-Driven Development of AngularJS Applications
All test are green
Feature #6 Complete
Feature #7
Persist tasks
git checkout -f feature_7_step_0
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
!
$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();
});
Red unit test
$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'}}
});
}]);;
$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);
};
Green unit test
All test are green
Feature #7 Complete
Feature #8
Task counter
git checkout -f feature_8_step_0
Feature UI
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
e2e scenario

describe("Task counter", function() {
it("should display number of visible tasks", function() {
expect(element(".js-task-counter").text()).toEqual("3 tasks");
});
});
Red e2e scenario
pluralize filter
{{filtered.length | pluralize:'task'}}

<tr ng-repeat="task in filtered = (tasks | filter:query)" ...>
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');
}));
});
Red unit test
pluralize filter
'use strict';
!
angular.module('AngularDoApp')
.filter('pluralize', function() {
return function(number, noun){
if (number == 0)
return "No " + noun + "s";
if (number == 1)
return number + " " + noun;
return number + " " + noun + "s";
}
});
Green unit test
Green e2e scenario
Test-Driven Development of AngularJS Applications
All test are green
Feature #8 Complete
grunt build
Questions?

More Related Content

Test-Driven Development of AngularJS Applications