ng-describe
Convenient BDD specs for Angular
Tested against angular v1.2, v1.3 and v1.4, dependent projects tested using dont-break - [![Circle CI] circle-icon ]circle-url.
Read Unit testing AngularJS using ng-describe tutorial, look through Unit testing slides.
Join Kensho and change the way financial industry analyzes information. We love open source and use the bleeding edge technology stack.
- Intro
- Install
- API
- Examples
- Development
- Note to Jasmine users
- Modules used
- License
Intro
Unit testing and mocking AngularJs requires a lot of boilerplate code:
;
ng-describe makes testing simple modules a breeze. Just list which modules you would like to load, which values / services / etc. you would like to inject and then start testing. Same test as above using ng-describe is much shorter and clearer:
;
ng-describe can inject dependencies, mock modules, set configs, create controllers, scopes, and even html fragments. For more details, continue reading. We also showed this library at AngularJS NYC meetup, the slides are at slides.com/bahmutov/ng-describe.
Install
npm install ng-describe --save-dev
Load ng-describe.js after angular and angular-mocks but before your specs, for example in Karma conf file.
// karma.conf.jsfiles: 'node_modules/angular/angular.js' 'node_modules/angular-mocks/angular-mocks.js' 'node_modules/ng-describe/dist/ng-describe.js' '<your source.js>' '<your specs.js>'
File dist/ng-describe.js
includes es5-shim and other dependencies needed by
the ngDescribe
function.
API
ng-describe provides a single function ngDescribe
that takes an options object.
;
You do not have to specify every option, there are reasonable defaults. We also tried to make the API user-friendly.
ngDescribe
returns itself, so you can chain multiple sets of specs easily
name: 'second suite' ...;
Primary options
name - a string name for the spec, similar to BDD describe(name, ...)
modules - list of modules to inject
angular;angular;;
If you have a single module to inject, you can just use a string name without Array notation
;
inject - list of dependencies to inject into unit tests. A single dependency can be just a string
without Array notation. All dependencies will be exposed as properties of the deps
argument to the
tests callback
angularvalue'foo' 42;;
tests - callback function that contains actual specs. Think of this as equivalent to describe
with
all necessary Angular dependencies taken care of.
;
Dependencies injection shortcut
You can list the dependencies to be injected directly in the test callback.
angular ;;
You can inject multiple providers, including built-in services. If the test callback argument
is named deps
or dependencies
it will be assumed that you do NOT use the shortcut.
The shortcut was implemented using changing named parameters trick.
mocks - top level mocks to be substituted into the tests. The mocks override any injected dependencies among modules.
;
For more information see examples below.
controllers - list of controllers by name that should be injected. Each controller
is created with a new $rootScope
instance.
NOTE: For each created controller, its SCOPE instance will be in the dependencies object.
angular ;;
element - HTML fragment string for testing custom directives and DOM updates.
;
The compiled angular.element
will be injected into the dependencies object under element
property.
See examples below for more information. The compilation will create a new scope object too.
parentScope - when creating HTML fragment, copies properties from this object into the
scope. The returned dependencies object will have deps.parentScope
that is the new scope.
// myFoo directive uses isolate scope for example;
See "2 way binding" example below.
configs - object with modules that have provider that can be used to inject run time settings. See Update 1 in Inject valid constants into Angular blog post and examples below.
Secondary options
verbose - flag to print debug messages during execution
only - flag to run this set of tests and skip the rest. Equivalent to ddescribe or describe.only.
;
skip - flag to skip this group of specs. Equivalent to xdescribe
or describe.skip
.
Could be a string message explaining the reason for skipping the spec.
exposeApi - expose low-level ngDescribe methods
The tests
callback will get the second argument, which is an object with the following methods
{
setupElement: function (elementHtml),
setupControllers: function (controllerNames)
}
You can use setupElement
to control when to create the element.
For example, instead of creating element right away, expose element factory so that you can create
an element after running a beforeEach
block. Useful for setting up mock backend before creating
an element.
;
See the spec in test/expose-spec.js
Or you can use setupControllers
to create controller objects AFTER setting up your spies.
angular ;
We need to listen for the foo
broadcast inside a unit test before creating the controller.
If we let ngDescribe
create the "broadcastController" it will be too late. Instead we
can tell the ngDescribe
to expose the low-level api and then we create the controllers when
we are ready
;
See the spec in test/controller-init-spec.js
http - shortcut for specifying mock HTTP responses,
built on top of $httpBackend.
Each GET request will be mapped to $httpBackend.whenGET
for example. You can provide
data, response code + data pair, response code + data + headers and optionally statusText
or custom function to return something using custom logic.
If you use http
property, then the injected dependencies will have http
object that
you can flush (it is really $httpBackend
object).
;
All standard methods should be supported (get
, head
, post
, put
, delete
, jsonp
and patch
).
Each of the methods can return a function that returns an configuration object, see mock http.
step - shortcut for running the digest cycle and mock http flush
{ ;}
Also flushes the mock http backend
http: {} { ;}
Also flushed the $timeout
service
root - alternative context for BDD callbacks
Imagine we are loading Angular and ngDescribe in a synthetic browser environment (like
jsdom). ngDescribe attaches itself to synthetic window
object, but the test framework callbacks are attached to global
object, not window
.
By passing an alternative object, we allow ngDescribe to discover it
, beforeEach
, etc.
// load ngDescribe in jsdom under Nodewindow
See repo ng-describe-jsdom for actual example that tests Angular without a browser, only a synthetic emulation.
Examples
Most examples use use the la
assertion from the
lazy-ass library and done callback argument
from Mocha testing framework.
Also, note that the dependencies object is filled only inside the unit test callbacks it
and
setup helpers beforeEach
and afterEach
;
Test value provided by a module
// A.jsangular value'foo' 'bar';// A-spec.js;
Test a filter
We can easily test a built-in or custom filter function
;
Test a service
We can inject a service to test using the same approach. You can even use multiple specs inside tests
callback.
// B.jsangular ;// B-spec.js;
Test controller and scope
We can easily create instances of controller functions and scope objects.
In this example we also inject $timeout
service to speed up delayed actions
(see Testing Angular async stuff).
angular ;;
Test directive
angular ;;
Test controllerAs syntax
If you use controllerAs
syntax without any components (see Binding to ... post or
Separate ...), then you can still test it quickly
angular ; ;
Test controller instance in custom directive
If you add methods to the controller inside custom directive, use controllerAs
syntax to
expose the controller instance.
angular ;;
Test 2 way binding
If a directive implements isolate scope, we can configure parent scope separately.
angular ;
We can use element
together with parentScope
property to set initial values.
;
We can change parent's values to observe propagation into the directive
// same setup;
beforeEach and afterEach
You can use multiple beforeEach
and afterEach
inside tests
function.
;
This could be useful for setting up additional mocks, like $httpBackend
.
angular ;;
Note if you use beforeEach
block with element
, the beforeEach
runs before the element
is created. This gives you a chance to setup mocks before running the element and possibly making calls.
If you really want to control when an element is created use exposeApi
option
(see Secondary options).
Mocking
Mock value provided by a module
Often during testing we need to mock something provided by a module, even if it is
passed via dependency injection. ng-describe makes it very simple. List all modules with values
to be mocked in mocks
object property.
// C.jsangular ;// C-spec.js;
Remember when making mocks, it is always module name : provider name : mocked property name
mocks: 'module name': 'mocked provider name': 'mocked value name'
Note: the mocked values are injected using $provider.constant
call to be able to override both
values and constants
angular ;;
You can even mock part of the module itself and use mock value in other parts via injection
angular ;;
Angular services inside mocks
You can use other injected dependencies inside mocked functions, using injected values and free parameters.
;
Mock $http.get
Often we need some dummy response from $http.get
method. We can use mock httpBackend
or mock the $http
object. For example to always return mock value when making any GET request,
we can use
mocks: ng: $http: { // inspect url if needed return $q; }
$http
service returns a promise that resolves with a response object. The actual result to send
is placed into the data
property, as I show here.
Mock http responses
You can use a shortcut to define mock HTTP responses via $httpBackend
module. For example,
you can define static responses.
;
All HTTP methods are supported (get
, post
, delete
, put
, etc.).
You can also get a function that would return a config object.
var mockGetApi = '/some/url': 42;mockGetApi'/some/other/url' = 500 'not ok';;
You can use deps.http.flush()
to move the http responses along.
You can return the entire http mock object from a function, or combine objects with functions.
{ return { return '/my/url': 42 ; } post: '/my/other/url': 200 'nice' ;};
You can use exact query arguments too
http: get: '/foo/bar?search=value': 42 '/foo/bar?search=value&something=else': 'foo' // $http.get('/foo/bar?search=value') will resolve with value 42// $http.get('/foo/bar?search=value&something=else') will resolve with value 'foo'
or you can build the query string automatically by passing params
property in the request config
objet
http: get: '/foo/bar?search=value&something=else': 'foo' // inside the unit testvar config = params: search: 'value' something: 'else' ;$http;
note the http
mocks are defined using $httpBack.when(method, ...)
calls,
which are looser than $httpBackend.expect(method, ...)
,
see ngMock/$httpBackend.
Spying
Spy on injected methods
One can quickly spy on injected services (or other methods) using sinon.js similarly to spying on the regular JavaScript methods.
- Include a browser-compatible combined sinon.js build into the list of loaded Karma files.
- Setup spy in the
beforeEach
function. Since every injected service is a method on thedeps
object, the setup is a single command. - Restore the original method in
afterEach
function.
// source codeangular ;
// spec;
Spy on injected function
You can inject a function, but use a Sinon spy instead
of the injected function to get additional information. For example, to spy on the $filter uppercase
,
we can use the following code.
;
Spy on 3rd party service injected some place else
Let us say you need to verify that the $interval
service injected in the module under test
was called. It is a little verbose to verify from the unit test. We must mock the $interval
with our function and then call the actual $interval
from the module ng
to provide the
same functionality.
Source code we are trying to unit test
angular ;
In the unit test we will mock $interval
service for module IntervalExample
// unit test startvar intervalCalled;;
A unit test just calls the numbers
function and then checks the variable intervalCalled
;
You can see the unit test in file test/spying-on-interval-spec.js.
Spy on mocked service
If we mock an injected service, we can still spy on it, just like as if we were spying on the regular service. For example, let us take the same method as above and mock it.
angular ;
The mock will return a different number.
;
Configure module
If you use a separate module with namesake provider to pass configuration into the modules (see Inject valid constants into Angular), you can easily configure these modules.
angular ;// config module has provider with same nameangular ;// spec file;
You can configure multiple modules at the same time. Note that during the configuration Angular is yet to be loaded. Thus you cannot use Angular services inside the configuration blocks.
Helpful failure messages
ng-describe works inside helpDescribe function, producing meaningful error messages on failure (if you use lazy assertions).
;
when this test fails, it generates meaningful message with all relevant information: the expression
that fails foo + bar === 4
and runtime values of foo
and bar
.
PhantomJS 1.9.7 (Mac OS X)
ட ngDescribe inside helpful
ட example
ட ✘ gives helpful error message FAILED
Error: condition [foo + bar === 4] foo: 2 bar: 3
at lazyAss (/ng-describe/node_modules/lazy-ass/index.js:57)
PhantomJS 1.9.7 (Mac OS X): Executed 37 of 38 (1 FAILED) (skipped 1) (0.053 secs / 0.002 secs)
Development
To build the README document, run unit tests and linter
npm run build
To run all unit tests (against different Angular versions)
npm test
To keep a watch and rerun build + lint + tests on source file change
npm run watch
For now, all source is in a single ng-describe.js
file, while the documentation
is generated from Markdown files in the docs
folder
To just run karma unit tests via Grunt plugin
npm run karma
If you have Karma runner installed globally you can run all the unit tests yourself ones
karma start --single-run=true test/karma.conf.js
Updating dependencies
This project uses a lot of 3rd party dependencies that constantly get out of date. To reliably update dependencies to the latest working versions, we use next-update. There is already a script command
npm run update-dependencies
You can upgrade a particular dependency by adding "-m ", for example
npm run update-dependencies -- -m jscs
If you use npm-quick-run you can use shorthand
nr u -m jscs
Note to Jasmine users
We got very tired of fighting bugs in the Jasmine test framework.
From the broken order of afterEach
callbacks to the afterAll
not firing at all - the work arounds
we had to write quickly becamse insane. Thus we
recommend Mocha testing
framework - fast, simple and seems to not suffer from any bugs. You do need your own assertion
framework, we use lazy-ass and a library
of predicates check-more-types.
Modules used
- check-more-types - Large collection of predicates.
- lazy-ass - Lazy assertions without performance penalty
License
Author: Kensho © 2014
Support: if you find any problems with this library, open issue on Github
The MIT License (MIT)
Copyright (c) 2014 Kensho
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.