Angular Testing
Presented By
Jimmy Tu
@jimee_02


Trends
- New frameworks are testable
- Testing skills on job postings
- Open source packages include tests
- Companies downsizing their QA
What the Industry realized
with test coverage
without test coverage
Complexity / Time
TDD teams experienced a 15-35% increase in initial development time after adopting TDD
http://research.microsoft.com/en-us/groups/ese/nagappan_tdd.pdf
Defects dropped 40% - 90%

What the Industry realized
Testing Saves Time
Time === $$$
Main Objective
Git Er Done
What Should You Test?
Rit: YES
Manager: Everything! ...but don't miss a deadline. AGILE!
Blogs: All the things
Spouse: What does A/B testing have to do with our marriage?!
Jasmine Structure
// xdescribe() {...} disables a test suite
// fdescribe() {...} focuses on a test suite
describe( 'A logical block/context with similar setup data to test', function() {
beforeEach(function(){
// Given
});
afterEach(function(){
// Cleanup or a consistent expectation across the test scenarios
});
// xit() {...} disable test
// fit() {...} focuses on a test
it('the situation that is being tested', function () {
// Some code that causes behavior / changes state
// an expectation of the result future state
expect(object.parameter).toEqual(true);
});
});describe('How to load your modules and retrieve your instances', function() {
var $httpBackend;
beforeEach(function(){
angular.mock.module('myAppModule'); // load the module and all its providers
angular.mock.module('anEntirelySeperateModuleNotRequiredByTheFirst');
});
// retrieve providers from module
beforeEach( angular.mock.inject( function (_$httpBackend_, _$http_, $injector) {
// either of these techniques will retrieve your instance
$httpBackend = _$httpBackend_;
$httpBackend = $injector.get('$httpBackend');
}));
afterEach(function(){
$httpBackend.verifyNoOutstandingRequest()
});
});Angular Structure
describe('Title Case Filter', function () {
var titleCaseFilter;
beforeEach(module('myModuleWithMyTestSubject'));
beforeEach(inject(function (_titleCaseFilter_) {
// Setup - Retrieve reference of your filter
titleCaseFilter = _titleCaseFilter_;
}));
it('should capitalize the first character of each word', function () {
var testText = 'the cake is a lie'; // Setup
var filteredText = titleCaseFilter(testText); // Execute behavior being tested
expect(filteredText).toBe('The Cake Is A Lie'); // Expect the outcome
});
it('should convert hyphens and underscores to spaces', function () {
// streamlined test cases into 1-liners
expect(titleCaseFilter('my-4-star-reivew')).toBe('My 1 Star Review');
expect(titleCaseFilter('my_1_star')).toBe('My 1 Star');
});
});Filters
describe('User Service', function () {
var userService;
var $httpBackend;
beforeEach(module('myModuleWithMyTestSubject'));
beforeEach(inject(function (_userService_, _$httpBackend_) {
userService = _userService_;
$httpBackend = _$httpBackend_;
}));
// async test
it('#getCurrentUser() should retrieve the current user profile', function (done) {
// Mocking request (Setup)
// use a regex to cut down on changes in the path, but make it descriptive enough
// to understand what endpoint it is trying to hit
$httpBackend.expectGET(/self/)).respond({id: 'test-id', username: 'test-username'};
// Execute behavior
userService.getCurrentUser()
.then(function(userProfile){
// Expectation
expect(userProfile.id).toEqual('test-id');
done(); // end the test
});
$httpBackend.flush();
});
});Service
Recap
Expectations
Mocking external dependencies
- Setup the test
- Cause the behavior
- Test for expected behavior
Test structure:

describe('User', function () {
it("profile", function () {
var testUserProfile = {"test-param", ''};
// mock call and return a promise
spyOn(userService, 'getCurrentUser').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve(testUserProfile);
return deferred.promise;
});
$scope = $rootScope.$new();
userController = $controller('UserController',
{$scope: $scope, userService: userService});
$scope.$apply(); // Cause a digest loop to resolve promises
expect(userService.getCurrentUser).toHaveBeenCalled();
expect($scope.user).toEqual(testUserProfile);
});
})The Man of few Words
it('should retrieve user information from the api', function(){
$httpBackend.whenGet('/users').respond({});
// ...a bunch of commented out code...
expect(true).toEqual(true);
});
it('should handle api errors gracefully', function(){
$httpBackend.whenGet('/users').respond(404);
// ...a bunch of commented out code...
expect(true).toEqual(true);
});The Liar
it('...', function(){
$httpBackend
.whenGet('http://domain.com:3000/users/uncles/aunts/brother-in-law').respond({});
});
it('...', function(){
$element.find('div>div>div>ul>li>a.profile-link').click();
});Mr. Glass
describe('User', function () {
it('user profile should display all their information and handles errors' +
'and does this... and...', function(done){
userService.getUsers()
.then(checkUserProfile)
.then(checkUsersLength)
.finally(done)
$httpBackend.flush(); // resolve pending http requests
$scope.$apply(); // q$ promises only resolve in a digest loop
function checkUserProfile (users) {
expect(users.status).toLessThanEqualTo('.99');
return users
}
function checkUsersLength (error) {
expect(users.length).toEqual(5);
}
});
});All-in-One
describe('Users Types', function () {
var userTypes;
beforeEach(function () {
dataForTest1And2 = DATA;
dataForTest1And3 = DATA;
dataForTest2And3And4 = DATA;
});
it('Test 1 user should ...', function(){};
it('Test 2 user should ...', function(){};
it('Test 3 user should ...', function(){};
it('Test 4 user should ...', function(){};
});One for All
it('...', function () {
var data = {
"inner": {
"max_position": "685025353593696256",
"has_more_items": false,
"items_html": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",
"new_latent_count": 0
},
"note": {
"d": { "status": "ok", "response": null },
"n": { "status": "ok", "response": { "latest": 1449624367, "toasts": [] } },
"b": {
"status": "ok",
"response": {
"count": 1,
"localized_count": "1",
"timestamp": -1,
"new_timestamp": -1,
"show_badge_highlighting": true,
"success": false
}
},
"t": { "status": "ok", "response": null }
}
}
expect(data.note.d.status).toEqual('ok');
});
Potential Post-coding-test-writing Poser
it("The 'toBeDefined' matcher compares against `undefined`", function() {
var a = {
foo: "foo"
};
expect(a.foo).toBeDefined();
expect(a.bar).not.toBeDefined();
});
it("The `toBeUndefined` matcher compares against `undefined`", function() {
var a = {
foo: "foo"
};
expect(a.foo).not.toBeUndefined();
expect(a.bar).toBeUndefined();
});
it("The 'toBeNull' matcher compares against null", function() {
var a = null;
var foo = "foo";
expect(null).toBeNull();
expect(a).toBeNull();
expect(foo).not.toBeNull();
});Your Father's Unit Test
describe('User Controller', function () {
var userService, $httpBackend, $controller, $scope;
beforeEach(module('myModuleWithMyTestSubject'));
beforeEach(inject(function (_userService_, _$httpBackend_, _$controller_, $rootScope) {
// ..save providers into variables
}));
describe('on initialization', function () {
var userController;
it("retrieves the current user's profile to display it", function () {
var testUserProfile = { "test-param", '' };
$httpBackend.expectGET(/self/).respond(testUserProfile);
$scope = $rootScope.$new();
userController = $controller('UserController', { $scope: $scope });
$httpBackend.flush();
expect($scope.user).toEqual(testUserProfile);
});
});
});Not your father's unit test (controller)
var user;
function getUser (id) {
if(!user) {
return userService.getUser(id)
.then(function (userProfile) {
user = userProfile;
return userProfile;
})
} else {
return user;
}
}Cludge
How To Test Reference
describe('User Controller', function () {
var userService, $httpBackend, $controller, $scope;
beforeEach(module('myModuleWithMyTestSubject'));
beforeEach(inject(function (_userService_, _$httpBackend_, _$controller_, $rootScope) {
// ..save providers into variables
}));
describe('on initialization', function () {
var userController;
it("should retrieve the current user's profile and save it to the vm", function () {
var testUserProfile = {"test-param", ''};
// mock call and return a promise
spyOn(userService, 'getCurrentUser').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve(testUserProfile);
return deferred.promise;
});
$scope = $rootScope.$new();
userController = $controller('UserController',
{$scope: $scope, userService: userService});
$scope.$apply(); // Cause a digest loop to resolve promises
expect(userService.getCurrentUser).toHaveBeenCalled();
expect($scope.user).toEqual(testUserProfile);
});
});
});Controller using a spy
Directive
describe('User Profile Directive', function () {
// .. variables
// beforeEach( ... );
// interaction tests
describe('on initialization', function () {
it("should display the user name when it is available", function () {
var $scope = $rootScope.$new();
var element = angular.element('<div user-profile="user"></div>');
$scope.user = {
name: 'test-name'
};
$compile(element)($scope);
$scope.$digest();
expect(element.html()).toContain($scope.user.name);
});
});
});Directive external function
describe('User Profile Directive', function () {
// .. variables
// beforeEach( ... );
// interaction tests
describe('after initialization', function () {
it("should execute the callback when the user has changed", function () {
var $scope = $rootScope.$new();
var element = angular.element('<div user-profile="user" ' +
'user-on-changed="updateStuff()"></div>');
$scope.user = {};
$scope.updateStuff = jasmine.createSpy('updateStuffSpy');
$compile(element)($scope);
$scope.$digest();
el.querySelector('.change-user').click();
expect($scope.updateStuff).toHaveBeenCalled();
});
});
});Directive with service (bad)
describe('User Profile Directive', function () {
// .. variables
beforeEach(function () {
angular.mock.module('myAppsModule', function($provide) {
$provide.decorator('userService', function($delegate) {
spyOn($delegate, 'getUser');
return $delegate;
});
})
});
// beforeEach( ... ); // injector
// interaction tests
describe('on initialization', function () {
it("updateUser() should retrieve new user information from the api", function () {
var $scope = $rootScope.$new();
var element = angular.element('<div user-profile="user"></div>');
$scope.user = {
name: 'test-name'
};
$compile(element)($scope);
$scope.$apply();
// ..logic to cause userService.getUser() to be executed
expect(userService.getUser).toHaveBeenCalled();
});});});Directive with service v2
describe('User Profile Directive', function () {
// .. variables
// beforeEach( ... );
// interaction tests
describe('on initialization', function () {
it("updateUser() should retrieve new user information from the api", function () {
var $scope = $rootScope.$new();
var element = angular.element('<div user-profile="user"></div>');
spyOn(userService, 'getUser').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve({});
return deferred.promise;
});
$scope.user = {
name: 'test-name'
};
$compile(element)($scope);
$scope.$apply();
// ..logic to cause userService.getUser() to be executed
expect(userService.getUser).toHaveBeenCalled();
});
});
});Directive controller method
describe('User Profile Directive', function () {
// .. variables
// beforeEach( ... );
// interaction tests
describe('on initialization', function () {
it("addNote() should add a note", function () {
var $scope = $rootScope.$new();
var element = angular.element('<div user-profile="user" user-on-changed="updateStuff()"></div>');
$scope.user = {};
$scope.updateStuff = jasmine.createSpy('updateStuffSpy');
$compile(element)($scope);
$scope.$digest();
controller = element.controller('UserProfile');
controller.addNote('');
expect($scope.notes.length).toBeGreaterThan(0);
});
});
});Directive requiring another directive
describe('User Profile Directive', function () {
// beforeEach( ... );
describe('when changing account tab', function () {
it("should not change the tab if the user is editing her profile", function () {
var $scope = $rootScope.$new();
// Wrap the directive in a containing element
var element = angular.element('<div><div user-profile="user"></div></div>');
var mockTabsController = jasmine.createSpyObj('mockTabsController', ['changeTab']);
var elementScope;
// Set tabsController in the containing element
// assumes that user-profile has a require: '^tabs'
element.data("$tabsController", mockTabsController);
$compile(element)($scope);
$scope.$apply();
elementScope = element.isolateScope();
elementScope.edit(); // turn on edit mode
// ...logic to cause a change in tab...
element.find('button').click();
expect(mockTabsController.changeTab).not.toHaveBeenCalled();
});
});
});Testing is an Art
There are no clear right vs wrong as long as your test covers the behavior properly.
Resilient
Protects
Informs
Coverage
Readable
Less Work
References/Resouces
Angular Testing
By jimee02
Angular Testing
A walkthrough of the path of least resistance when writing tests in Angular.js app.
- 1,240