Angular Unit Testing Helpers
Table of Contents
Why?
I've created this package to simplify unit testing in AngularJS apps. I had enough of writing repeated code. For every spec (controller, directive, service) I had to write the same injector and compile blocks of code, for every mocked service I had to write the same lines. With this package, everything becomes easier and faster.
Features
All selectors are using native Javascript querySelector
or querySelectorAll
, so jQuery
is not requierd.
Installation:
- Download package:
npm install angular-unit-testing-helpers
or
bower install angular-unit-testing-helpers
- Inject it to
karma.conf.js
files: [
'node_modules/angular-unit-testing-helpers/test-helpers.js',
...
],
or
files: [
'bower_components/angular-unit-testing-helpers/test-helpers.js',
...
],
- Or import directly from package
import { TestServ, TestElement } from 'angular-unit-testing-helpers';
const TestServ = require('angular-unit-testing-helpers').TestServ;
const TestElement = require('angular-unit-testing-helpers').TestElement;
Back to top
TypeScript:
You can use this library with TypeScript. All you need to do:
- Have typings installed (1.x):
npm install typings
- Have installed angular, angular-mocks and jquery typings
typings install dt~jquery --save --global
typings install dt~angular --save --global
typings install dt~angular-mocks --save --global
- Have installed typings in angular-unit-testing-helpers
typings install npm:angular-unit-testing-helpers
Back to top
TestServ documentation
TestServ contructor:
Without an argument:
new TestServ()
It will create an empty object;
With an argument:
new TestServ('$q');
It will return real $q
service;
Implementation:
window.TestServ = function(name) {
var _this = this;
if (!!name) {
inject([name, function(service) {
_this = service;
}]);
return _this;
}
};
Back to top
addMethod:
var someService = new TestServ();
someService.addMethod(name, returnedValue);
addMethod
will add an empty function to the someService at name
value and also create spyOn on this created method. spyOn will return returnedValue
.
returnedValue
can be undefined, a value, an object or a function.
You can also construct chaining methods calls, for example:
var someService = new TestServ();
someService.addMethod('firstMethod').addMethod('secondMethod').addMethod('thirdMethod');
More in examples.
Implementation:
addMethod: function(name, returnedValue) {
if (typeof returnedValue === "function" ) {
this[name] = returnedValue;
spyOn(this, name).and.callThrough();
} else {
this[name] = angular.noop;
spyOn(this, name).and.returnValue(returnedValue !== undefined ? returnedValue : this);
}
return this;
}
Back to top
addPromise:
var someService = new TestServ();
someService.addPromise(name);
addPromise
will add an empty function to the someService at name
value and also create spyOn on this created method. Same as addMethod
.
But spyOn will return object with then
property, which will become a function. aThis function will bind two arguments to success
and error
property of someService[name]
. So to call success promise you will simply call someService[name].success()
and for failure promise someService[name].fail
. You can also call this function with arguments (someService[name].success('someString')
), so when you call this someService[name].then(function(response) { console.log(response)}), response will become
'someString'`.
You can also construct chaining methods calls, for example:
var someService = new TestServ();
someService.addMethod('firstMethod').addMethod('secondMethod').addPromise('promise');
Implementation:
addPromise: function(name) {
var _this = this;
_this[name] = function() {};
spyOn(_this, name).and.returnValue({
then: function(success, fail) {
_this[name].success = success;
_this[name].fail = fail;
}
});
return this;
}
addProperty:
var someService = new TestServ();
someService.addProperty(name, returnedValue);
addProperty
will add a property to the someService with returnedValue as a value.
You can also construct chaining methods calls, for example:
var someService = new TestServ();
someService.addProperty('someProperty', propertyValue).addProperty('someOtherProperty', otherPropertyValue);
More in examples.
Implementation:
addProperty: function(name, returnedValue) {
this[name] = returnedValue;
return this;
}
get:
var someService = new TestServ();
someService.get(name);
get
will return property from a created service. You can use direct propery call (someService.name), this method is useful with typescript. More in typescript chapter.
someService.get(name) === someService.name;
Implementation:
get: function(name) {
return this[name];
}
Back to top
TestServ examples
TestServ examples
TestElement documentation
TestElement contructor:
new TestElement();
It will create an object, which will contain some angular services: $rootScope
, $compile
, $timeout
, $controller
, $templateCache
;
Implementation:
window.TestElement = function() {
var _this = this;
inject(function($rootScope, $compile, $timeout, $controller, $templateCache, $filter) {
_this._$scope = $rootScope.$new();
_this.$originalScope = $rootScope.$new();
_this.$compile = $compile;
_this.$timeout = $timeout;
_this.$controller = $controller;
_this.$templateCache = $templateCache;
_this.$filter = $filter;
});
this.name = '';
};
Back to top
createCtrl:
var element, someController, services = {
someSrevice: mockedSomeService
};
element = new TestElement();
someController = element.createCtrl(name, services);
createCtrl
will create and return a controller with 'name' and services object. You don't need to inject $scope
into services
method, it's injected by default if services.$scope doesn't exists.
Implementation:
createCtrl: function(name, services) {
if (!services) {
services = {};
}
if (!services.$scope) {
services.$scope = this._$scope;
}
this._ctrl = this.$controller(name, services);
return this._ctrl;
}
Back to top
addTemplate:
var element;
element = new TestElement();
element.createCtrl(name);
element.addTemplate(path, ctrlAs);
addTemplate
will create and return an angular element with current $scope. path
is a path to the template that is stored in $templateCache. ctrlAs is an optional argument. If you are using controllerAs
syntax, then ctrlAs
should be the value of controllerAs
property.
Implementation:
addTemplate: function(path, ctrlAs) {
var template;
template = this.$templateCache.get(path);
this._el = angular.element(template);
if (!!ctrlAs) {
this._$scope[ctrlAs] = this._ctrl;
}
this.$compile(this._el)(this._$scope);
this._$scope.$digest();
try {
this.$timeout.verifyNoPendingTasks();
} catch (e) {
this.$timeout.flush();
}
return this._el;
}
Back to top
createDirective:
var element;
element = new TestElement();
element.createDirective(name, html, scope);
createDirective
will create and return an angular element with with html
and scope
. name
is a name of the directive, html
is a string and scope
is an object, e. g.: name = 'someDirective', html = '<some-directive attribute="someValue"></some-directive>'; scope = { someValue: 123 };
.
Implementation:
createDirective: function(name, html, scope) {
this.name = name;
this._$scope = angular.extend(this.$originalScope, scope);
this._el = this.$compile(elem)(this._$scope);
this._$scope.$digest();
try {
this.$timeout.verifyNoPendingTasks();
} catch (e) {
this.$timeout.flush();
}
return this._el;
}
Back to top
createComponent:
var element;
element = new TestElement();
element.createComponent(name, html, scope);
createComponent
is an alias to createDirective method.
Implementation:
createComponent: function(name, html, scope) {
this.createDirective(name, html, scope);
return this._el;
}
Back to top
createFilter:
var filter;
filter = new TestElement().createFilter(name);
createFilter
will return filter with given name.
Implementation:
createFilter: function(name) {
return this.$filter(name);
}
Back to top
get scope:
element.scope
scope
will return current scope of the element.
Implementation:
get scope() {
return this._ctrl ?
this._$scope : Object.keys(this.dom.children()).length ?
this.dom.children().scope() : this.dom.scope();
}
Back to top
get ctrl:
element.ctrl
ctrl
will return controller created with createCtrl
method or controller created with createDirective
method.
Implementation:
get ctrl() {
return this._ctrl ? this._ctrl : angular.element(this._el).controller(this.name);
}
Back to top
get dom:
element.dom
dom
will return current angular element of the template created with addTemplate
or the directive created with createDirective
.
Implementation:
get dom() {
return angular.element(this._el);
}
Back to top
find:
element.find(selector)
find
will return found angular element with selector
.
Implementation:
find: function (selector) {
return angular.element(this.dom[0].querySelector(selector));
}
Back to top
findAll:
element.findAll()
findAll
will return all found angular elements with selector
as an array.
Implementation:
findAll: function (selector) {
var htmlObject = this.dom[0].querySelectorAll(selector);
var returnedArray = [];
for (var property in htmlObject) {
if (htmlObject.hasOwnProperty(property) && typeof htmlObject[property] !== 'number') {
returnedArray.push(angular.element(htmlObject[property]));
}
}
return returnedArray;
}
Back to top
destroy:
element.destroy()
destroy
will destroy current element.
Implementation:
destroy: function() {
this._$scope.$destroy();
this._el = null;
this._ctrl = null;
}
Back to top
clickOn:
element.clickOn(selector);
clickOn
will click on element found with selector
and make a $scope.$digest()
. It returns a promise.
Implementation:
clickOn: function(selector) {
if (this.dom[0].querySelector(selector)) {
this.dom[0].querySelector(selector).click();
} else {
this.dom[0].click();
}
this._$scope.$digest();
return this._getFlushedThenable();
}
Back to top
inputOn:
element.inputOn(selector, value, which);
inputOn
will set value of the element found with selector
, trigger a input
handler and make $scope.$digest()
. It returns a promise.
If you have many inputs with the same selector, then you can pass which input you want to react with by adding number as a third argument (0 is a first input). which
is an optional argument.
Implementation:
inputOn: function(selector, value, which) {
if (!which) {
if (this.dom[0].querySelector(selector)) {
angular.element(this.dom[0].querySelector(selector)).val(value).triggerHandler('input');
} else if (this.dom[0].tagName == 'INPUT') {
this._el.val(value).triggerHandler('input');
}
} else {
if (this.dom[0].querySelectorAll(selector)[which]) {
angular.element(this.dom[0].querySelectorAll(selector)[which]).val(value).triggerHandler('input');
} else if (this.dom[0].tagName == 'INPUT') {
this._el.val(value).triggerHandler('input');
}
}
this._$scope.$digest();
return this._getFlushedThenable();
}
Back to top
TestElement examples
TestElement examples
Back to top
TestModule documentation
TestModule contructor:
new TestModule(name);
It will create an module object with given name
;
Implementation:
window.TestModule = function(name) {
this.module = angular.module(name);
this.deps = this.module.value(name).requires;
};
Back to top
hasModule:
var someModule = new TestModule(moduleName);
someModule.hasModule(dependencyModule);
hasModule
will return boolean
value: true
if moduleName
has dependencyModule as a dependency and false
if not.
Implementation:
hasModule: function(name) {
return this.deps.indexOf(name) >= 0;
}
Back to top
TestModule examples
TestModule examples
Back to top
TestFactory documentation
define:
TestFactory.define(name, attributes);
define
will define a model with attributes
for creating factories (it can be also a sequence
). name
should be unique. It should be called before any create action. The best solution is to define models in seperate folder and inject it at the beginning of the karma.config
file (but after test-helpers
).
Example:
TestFactory.define('user', {
name: 'someName',
id: 123,
pet: {
type: 'cat',
name: 'Tom'
},
friends: ['Neo', 'Trinity', 'Morfeus']
});
Back to top
create:
TestFactory.create(name, attributes)
create
will create an object with model named name
. attributes
are an optional argument, it overwrites default attributes defined with define
method.
Example:
user = TestFactory.create('user', {
name: 'John',
pet: {
name: 'Jerry',
type: 'mouse'
});
Back to top
createList:
TestFactory.createList(name, number, attributes)
createList
will create an collection of object with model named name
. number
defines how many objects in collections should be added. attributes
are an optional argument, it overwrites default attributes defined with define
method.
Example:
users = TestFactory.createList('user', 3, {
name: 'John',
pet: {
name: 'Jerry',
type: 'mouse'
});
Back to top
defineSequence:
TestFactory.defineSequence(name, argOne, argTwo)
defineSequence
will define a model with attributes
for creating factories. name
should be unique. It should be called before any sequence call. argOne
can be iterator or function.
Example:
TestFactory.defineSequence('simpleSeq');
TestFactory.defineSequence('seqWithIterator', 4);
TestFactory.defineSequence('seqWithFunction', function(value) {
return 'Name ' + value;
});
TestFactory.defineSequence('seqWithFunctionAndIterator', function(value) {
return 'Age ' + value;
}, 5);
Back to top
sequence:
TestFactory.sequence(name);
TestFactory.sequence(name);
sequence
returns a function. When you call it, then sequnce will increment. When you call clear
method on it, then sequnce will be cleared to default value;
Example:
TestFactory.defineSequence('simpleSeq');
TestFactory.sequence('simpleSeq')();
TestFactory.sequence('simpleSeq')();
TestFactory.sequence('simpleSeq').clear();
TestFactory.sequence('simpleSeq')();
TestFactory.sequence('simpleSeq')();
Back to top
TestFactory examples
TestFactory examples
TestDummy documentation
filter:
TestDummy.filter;
filter
will return the simpliest filter. It's useful when filter is in another module.
Implementation:
get filter() {
return function(input) {
return input;
};
}
Back to top
directive:
TestDummy.directive
directive
will return the simpliest directive. It's useful when directive is in another module.
Implementation:
get directive() {
return [{ restrict: 'AE' }];
}
Back to top
TestDummy examples
TestDummy examples