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
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
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>
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
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>
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
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>
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
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
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">
Green e2e scenario
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
All test are green
Feature #8 Complete
grunt build
Questions?

Test-Driven Development of AngularJS Applications