- Why?
- Features
- Installation
- TypeScript
- TestServ documentation
- TestServ examples
- TestElement documentation
- TestElement examples
- TestModule documentation
- TestModule examples
- TestFactory documentation
- TestFactory examples
- TestDummy documentation
- Dummy examples
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.
All selectors are using native Javascript querySelector
or querySelectorAll
, so jQuery
is not requierd.
- 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;
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
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;
}
};
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;
}
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;
}
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;
}
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];
}
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 = '';
};
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;
}
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;
}
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;
}
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;
}
var filter;
filter = new TestElement().createFilter(name);
createFilter
will return filter with given name.
Implementation:
createFilter: function(name) {
return this.$filter(name);
}
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();
}
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);
}
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);
}
element.find(selector)
find
will return found angular element with selector
.
Implementation:
find: function (selector) {
return angular.element(this.dom[0].querySelector(selector));
}
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;
}
element.destroy()
destroy
will destroy current element.
Implementation:
destroy: function() {
this._$scope.$destroy();
this._el = null;
this._ctrl = null;
}
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();
}
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();
}
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;
};
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;
}
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']
});
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'
});
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'
});
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'); // => 1,2,3...
TestFactory.defineSequence('seqWithIterator', 4); // => 4,8,12...
TestFactory.defineSequence('seqWithFunction', function(value) {
return 'Name ' + value;
}); // => 'Name 1', 'Name 2', 'Name 3'...
TestFactory.defineSequence('seqWithFunctionAndIterator', function(value) {
return 'Age ' + value;
}, 5); // => 'Age 5', 'Age 10', 'Age 15'...
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')(); // => 1
TestFactory.sequence('simpleSeq')(); // => 2
TestFactory.sequence('simpleSeq').clear();
TestFactory.sequence('simpleSeq')(); // => 1
TestFactory.sequence('simpleSeq')(); // => 2
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;
};
}
TestDummy.directive
directive
will return the simpliest directive. It's useful when directive is in another module.
Implementation:
get directive() {
return [{ restrict: 'AE' }];
}