ng-test-utils
Cleaner, less verbose tests for your Angular app!
![Coverage Status](https://coveralls.io/repos/jamestalmage/angular-test-utils/badge.svg?branch=master)
Angular is an amazing framework, but its dependency injection framework can lead to some pretty verbose tests:
module('myModule');
var serviceA;
beforeEach(module(function() {
serviceA = sinon.spy();
$provide.value('serviceA serviceA);
}));
var serviceB, $rootScope, scope;
beforeEach(inject(function(_serviceB_, _$rootScope_) {
serviceB = _serviceB_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
}));
// Finally - I can start writing tests!!
All those beforeEach
statements start to add up.
They clutter the top of all your test files, and distract from what's important.
ng-test-utils
seeks to offer a better way:
module('myModule');
var serviceA = sinon.spy();
var serviceB, $rootScope, scope = $rootScope.$new();
ng-test-utils
automatically generates the rest for you through a series of ast transforms.
Better still, it includes comprehensive source-map support to help you easily identify exactly
where errors are being thrown in your code. It even plays nice with upstream transforms that supply
source-maps (i.e. coffee-script).
It includes a comprehensive tests suite (with 100% coverage), and has a thorough complement
of plugins with examples that will help you fit it in to your build process.
@ngInject
The @ngInject
annotation allows you to inject instances from the Angular dependency injection framework
directly in to your tests with ease. Variable names are used to infer the instance name you want injected, so
var myService;
Will cause the "myService" instance from your module to be injected in to the test.
This injection is automatically wrapped in a beforeEach
method call compatible with mocha
or jasmine
.
After ng-test-utils
does its thing, the final code looks something like this.
var myService;
beforeEach(inject(function(_myService_) {
myService = _myService_;
}));
That's great, but let's say a significant portion of your tests are focused on a member of myService that
is a few layers deep: myService.some.thing
. Ignore for a moment that such a structure is probably
a bad idea. This is easily accomplished my using @ngInject
on a variable declaration that includes
an initialization.
var thing = myService.some.thing;
This will inject myService
within a beforeEach
, and assign the appropriate value to your variable.
The transformed code looks like this:
var thing;
beforeEach(inject(function(myService) {
thing = myService.some.thing;
}));
You could combine both approaches (giving your tests access to both myService
and thing
as follows:
var myService, thing = myService.some.thing;
var myService, thing;
beforeEach(inject(function(_myService_) {
myService = _myService_;
thing = myService.some.thing;
}));
You can even initialize variables with the results of method calls on injected items.
var scope = $rootScope.$new();
var scope;
beforeEach(inject(function($rootScope){
scope = $rootScope.$new();
}));
@ngInjectProvider
Works identically to @ngInject
but performs it's injections during the config
stage instead
of the run
stage of module initialization. This means you have access to instance providers
instead of instances.
var $compileProvider;
var scope;
beforeEach(module(function(_$compileProvider_){
$compileProvider = _$compileProvider_;
}));
@ngValue & @ngProvide
Where @ngInject
helps you get testable instances out of your angular module, @ngValue
provides a way to place spies or mocks in to the dependency injection framework. Variable names
are used to infer the name for the item being injected.
var myService = {
doSomething: sinon.spy(),
somethingElse: sinon.spy()
};
var myService;
beforeEach(module(function($provide){
myService = {
doSomething: sinon.spy(),
somethingElse: sinon.spy()
};
$provide.value('myService myService);
}));
@ngProvide
works the same way and is synonymous with @ngValue
, but I plan to repurpose it with slightly
different semantics - so stick with @ngvalue
for now.
You can use both @ngValue
and @ngInject
together in your tests, but you must make sure all of your
@ngValue
declarations come before your first @ngInject
.
@ngConstant
deprecated - Angular constants can not be overridden, so this is pretty useless.
Use providers for configurable application wide settings.
Very similar to @ngConstant
, but it provides a constant service. From the angular docs:
Unlike a value [a constant] can be injected into a module configuration function (see angular.Module)
and it cannot be overridden by an Angular decorator
var siteUrl = 'https://angular.io';
var siteUrl;
beforeEach(module(function($provide){
siteUrl = 'https://angular.io';
$provide.constant('siteUrl siteUrl);
}));
All your @ngConstant
declarations must come before your first @ngInject
.
@ngFactory
Provides a way to inject mock services using Angulars factory style provider.
A factory function takes a list of injectables as arguments and returns a service.
When you place the @ngFactory
annotation on a named function, it will be replaced
by a variable with that same name that is injected with the result of the factory
functions invocation.
function timeoutInSeconds($timeout) {
return function(fn, delay, invokeApply) {
return $timeout(fn, delay * 1000, invokeApply);
};
}
var timeoutInSeconds;
beforeEach(module(function($provide) {
$provide.factory(function($timeout) {
return timeoutInSeconds = function(fn, delay, invokeApply) {
return $timeout(fn, delay * 1000, invokeApply);
};
});
}));
The above example could also be achieved by combining @ngValue
and @ngInject
.
@ngValue
var timeoutInSeconds = function(fn, delay, invokeApply) {
return $timeout(fn, delay, invokeApply);
}
@ngInject
var $timeout;
This will work in most cases, and has the added advantage of exposing $timeout to
your tests as well. Problems would arise if timeoutInSeconds
were to be called
during module initialization before the @ngInject
annotation has injected $timeout
in to your test. In that case @ngFactory
is an acceptable workaround.
@ngService
Very similar to @ngFactory
, but rather than assigning the return value, it
uses the function as a constructor and injects the new instance.
function myService(injectedDependency) {
this.foo = "bar";
}
var myService;
beforeEach(module(function($provide) {
$provide.service("myService", function(injectedDependency) {
myService = this;
this.foo = "bar";
});
});
Note - this currently does not work if you return
a value from your constructor.
If that is the case you probably should be using @ngFactory
.
@ngProvider
Provide mock providers using $provide.provider()
var greetProvider = {
name: "world",
$get: function(a) {
return "hello " + this.name + a;
}
}
var greetProvider;
beforeEach(module(function($provide) {
greetProvider = {
name: "world",
$get: function(a) {
return "hello " + this.name + a;
}
};
$provide.provider("greet", myProvider);
}));
Note that if your variable name has a "Provider" suffix, it will be stripped off when
creating the name for the service. This allows you to avoid naming collisions when
your tests need access to both the provider and the provided instance:
function greetProvider() { }
var greet;
@ngDirective
Create a stub directive (experimental).
Possible way to test how directive controllers interact with children.
This does not allow you to swap out directives for mocks
(see replaceDirectiveController for that).
function myDirective($timeout, $log) {
return {
require: "^parentDirective",
template: "<div></div>",
link: function postLink(scope, iElement, iAttrs, controller) {
}
};
}
var myDirective;
beforeEach(module(function($compileProvider) {
myDirective = function($timeout, $log) {
return {
require: "^parentDirective",
template: "<div></div>",
link: function postLink(scope, iElement, iAttrs, controller) {
}
}
};
$compileProvider.directive("myDirective", myDirective);
}));
@replaceDirectiveController
Angular does not provide a straightforward way to swap out directive implementations.
This annotation will allows you to swap out the controller for testing.
The controller function will be swapped out with a variable of the same name, assigned to an array.
Each time your controller stub is initialized by Angular, the new instance will be pushed in to that array.
(note: this does not yet work with controller functions that return a value it pushes "this" on to the array);
function customClick() {
this.doSomething = sinon.spy();
}
var customClick = [];
beforeEach(module(function($provide) {
$provide.decorator("customClickDirective", function($delegate) {
var directive = $delegate[0];
directive.controller = function() {
customClick.push(this);
this.doSomething = sinon.spy();
};
return $delegate;
});
}));
source-maps
ng-test-utils
uses recast to scan your code and inject all the
boilerplate required to make things work (it injects the beforeEach
methods exactly as shown above).
However, modified javascript can create difficulties during the debug process if the line numbers displayed
when an Error gets thrown do not match the actual place in your code where it is happening. Fortunately,
ng-test-utils
ships with full source-map support. Just make sure you enable source-maps
in your browsers developer tools, and enable source-map support from whichever plugin you
are using in your build.
build plugins
ng-test-utils
has a number of companion plugins that help you insert it in your build process.
Each one has its own set of examples that will help get you started.
-
The karma preprocessor provides
the simplest solution that will work for the majority of users. Since the transforms provided
are entirely testing related, most users will not need anything beyond this.
-
The browserify transform,
provides a way to perform the injections on code before it gets bundled by browserify.
Combine with karma-browserify to test your
CommonJS compatible angular modules.
-
The gulp plugin provides a gulp compatible
transform. Use gulps powerful streaming system to wire the transform up to any input/output stream you choose.