UNPKG

40.4 kBMarkdownView Raw
1# ng-describe
2
3> Convenient BDD specs for Angular
4
5[![NPM][ng-describe-icon] ][ng-describe-url]
6[![Quality][quality-badge] ][quality-url]
7
8[![Build status][ng-describe-ci-image] ][ng-describe-ci-url]
9[![manpm](https://img.shields.io/badge/manpm-%E2%9C%93-3399ff.svg)](https://github.com/bahmutov/manpm)
10[![Codacy Badge][ng-describe-codacy-image] ][ng-describe-codacy-url]
11[![semantic-release][semantic-image] ][semantic-url]
12
13[![Coverage Status][ng-describe-coverage-image] ][ng-describe-coverage-url]
14[![dependencies][ng-describe-dependencies-image] ][ng-describe-dependencies-url]
15[![devdependencies][ng-describe-devdependencies-image] ][ng-describe-devdependencies-url]
16
17Tested against angular v1.2, v1.3 and v1.4,
18dependent projects tested using [dont-break][dont-break] - [![Circle CI] [circle-icon] ][circle-url].
19
20Read [Unit testing AngularJS using ng-describe](http://glebbahmutov.com/blog/1-2-3-tested/) tutorial,
21look through [Unit testing](http://slides.com/bahmutov/ng-describe) slides.
22
23Join [Kensho](https://kensho.com/#/careers) and change the way financial industry analyzes information.
24We love open source and use the bleeding edge technology stack.
25
26[ng-describe-icon]: https://nodei.co/npm/ng-describe.png?downloads=true
27[ng-describe-url]: https://npmjs.org/package/ng-describe
28[ng-describe-ci-image]: https://travis-ci.org/kensho/ng-describe.png?branch=master
29[ng-describe-ci-url]: https://travis-ci.org/kensho/ng-describe
30[ng-describe-coverage-image]: https://coveralls.io/repos/kensho/ng-describe/badge.png
31[ng-describe-coverage-url]: https://coveralls.io/r/kensho/ng-describe
32[ng-describe-dependencies-image]: https://david-dm.org/kensho/ng-describe.png
33[ng-describe-dependencies-url]: https://david-dm.org/kensho/ng-describe
34[ng-describe-devdependencies-image]: https://david-dm.org/kensho/ng-describe/dev-status.png
35[ng-describe-devdependencies-url]: https://david-dm.org/kensho/ng-describe#info=devDependencies
36[ng-describe-codacy-image]: https://www.codacy.com/project/badge/25cb5d1410c7497cb057d887d1f3ea23
37[ng-describe-codacy-url]: https://www.codacy.com/public/kensho/ng-describe.git
38[semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
39[semantic-url]: https://github.com/semantic-release/semantic-release
40
41[quality-badge]: http://npm.packagequality.com/badge/ng-describe.png
42[quality-url]: http://packagequality.com/#?package=ng-describe
43
44[circle-icon]: https://circleci.com/gh/kensho/ng-describe.svg?style=svg
45[circle-url]: https://circleci.com/gh/kensho/ng-describe
46[dont-break]: https://github.com/bahmutov/dont-break
47
48
49
50* [Intro](#intro)
51* [Install](#install)
52* [API](#api)
53 * [Primary options](#primary-options)
54 * [Secondary options](#secondary-options)
55* [Examples](#examples)
56 * [Test value provided by a module](#test-value-provided-by-a-module)
57 * [Test a filter](#test-a-filter)
58 * [Test a service](#test-a-service)
59 * [Test controller and scope](#test-controller-and-scope)
60 * [Test directive](#test-directive)
61 * [Test controllerAs syntax](#test-controlleras-syntax)
62 * [Test controller instance in custom directive](#test-controller-instance-in-custom-directive)
63 * [Test 2 way binding](#test-2-way-binding)
64 * [beforeEach and afterEach](#beforeeach-and-aftereach)
65 * [Mocking](#mocking)
66 * [Mock value provided by a module](#mock-value-provided-by-a-module)
67 * [Angular services inside mocks](#angular-services-inside-mocks)
68 * [Mock $http.get](#mock-httpget)
69 * [Mock http responses](#mock-http-responses)
70 * [Spying](#spying)
71 * [Spy on injected methods](#spy-on-injected-methods)
72 * [Spy on injected function](#spy-on-injected-function)
73 * [Spy on 3rd party service injected some place else](#spy-on-3rd-party-service-injected-some-place-else)
74 * [Spy on mocked service](#spy-on-mocked-service)
75 * [Configure module](#configure-module)
76 * [Helpful failure messages](#helpful-failure-messages)
77* [Development](#development)
78 * [Updating dependencies](#updating-dependencies)
79* [Modules used](#modules-used)
80* [License](#license)
81
82
83## Intro
84
85Unit testing and mocking AngularJs requires a lot of boilerplate code:
86```js
87describe('typical test', function () {
88 var foo;
89 beforeEach(function () {
90 angular.mock.module('A');
91 // other modules
92 });
93 beforeEach(inject(function (_foo_) {
94 foo = _foo_;
95 }));
96 it('finally a test', function () {
97 expect(foo).toEqual('bar');
98 });
99});
100```
101
102ng-describe makes testing simple modules a breeze.
103Just list which modules you would like to load, which values / services / etc.
104you would like to inject and then start testing. Same test as above using ng-describe
105is much shorter and clearer:
106```js
107ngDescribe({
108 modules: 'A',
109 tests: function (foo) {
110 it('finally a test', function () {
111 expect(foo).toEqual('bar');
112 });
113 }
114});
115```
116ng-describe can inject dependencies, mock modules, set configs, create controllers, scopes, and
117even html fragments. For more details, continue reading. We also showed this library at AngularJS NYC
118meetup, the slides are at [slides.com/bahmutov/ng-describe](http://slides.com/bahmutov/ng-describe).
119
120
121## Install
122
123`npm install ng-describe --save-dev`
124
125Load ng-describe.js after angular and angular-mocks but before your specs, for example in Karma conf file.
126
127```js
128// karma.conf.js
129files: [
130 'node_modules/angular/angular.js',
131 'node_modules/angular-mocks/angular-mocks.js',
132 'node_modules/ng-describe/dist/ng-describe.js',
133 '<your source.js>',
134 '<your specs.js>'
135],
136```
137
138File `dist/ng-describe.js` includes es5-shim and other dependencies needed by
139the `ngDescribe` function.
140
141
142## API
143
144ng-describe provides a single function `ngDescribe` that takes an options object.
145
146```js
147ngDescribe({
148 // your options
149});
150```
151
152You do not have to specify every option, there are reasonable defaults. We also tried to make
153the API [user-friendly](http://glebbahmutov.com/blog/user-friendly-api/).
154
155`ngDescribe` returns itself, so you can chain multiple sets of specs easily
156
157```js
158ngDescribe({
159 name: 'first suite'
160 ...
161})({
162 name: 'second suite'
163 ...
164});
165```
166
167### Primary options
168
169**name** - a string name for the spec, similar to BDD `describe(name, ...)`
170
171**modules** - list of modules to inject
172
173```js
174angular.module('A', []);
175angular.module('B', []);
176ngDescribe({
177 name: 'modules example',
178 modules: ['A', 'B']
179});
180```
181
182If you have a single module to inject, you can just use a string name without Array notation
183
184```js
185ngDescribe({
186 name: 'single module',
187 modules: 'A'
188});
189```
190
191**inject** - list of dependencies to inject into unit tests. A single dependency can be just a string
192without Array notation. All dependencies will be exposed as properties of the `deps` argument to the
193tests callback
194
195```js
196angular.module('A', []).value('foo', 42);
197ngDescribe({
198 name: 'inject example',
199 modules: 'A',
200 inject: ['foo', '$timeout'],
201 tests: function (deps) {
202 it('has foo', function () {
203 expect(deps.foo).toEqual(42);
204 });
205 it('has timeout service', function () {
206 expect(typeof deps.$timeout).toEqual('function');
207 });
208 }
209});
210```
211
212**tests** - callback function that contains actual specs. Think of this as equivalent to `describe` with
213all necessary Angular dependencies taken care of.
214
215```js
216ngDescribe({
217 inject: ['$q', '$rootScope'],
218 tests: function (deps) {
219 it('injects $q', function () {
220 expect(typeof deps.$q).toEqual('function');
221 });
222 it('can be resolved', function () {
223 deps.$q.when(42).then(function (value) {
224 expect(value).toEqual(42);
225 });
226 // move promises along
227 deps.$rootScope.$digest();
228 });
229 }
230});
231```
232
233**Dependencies injection shortcut**
234
235You can list the dependencies to be injected directly in the test callback.
236
237```js
238angular.module('shortcut', [])
239 .constant('foo', 'bar');
240ngDescribe({
241 module: 'shortcut',
242 tests: function (foo) {
243 it('has constant', function () {
244 console.assert(foo === 'bar');
245 });
246 }
247});
248```
249
250You can inject multiple providers, including built-in services. If the test callback argument
251is named `deps` or `dependencies` it will be assumed that you do NOT use the shortcut.
252
253The shortcut was implemented using [changing named parameters trick][trick].
254
255[trick]: http://glebbahmutov.com/blog/changing-the-function-arguments-trick/
256
257**mocks** - top level mocks to be substituted into the tests.
258The mocks override *any* injected dependencies among modules.
259
260```js
261ngDescribe({
262 mocks: {
263 // each module to mock by name
264 moduleName1: {
265 // each dependency from moduleName1 to mock
266 dependencyName1: mockValue1,
267 dependencyName2: mockValue2
268 // the rest of moduleName1 is unchanged
269 },
270 moduleName2: {
271 // dependencies to mock in moduleName2
272 }
273 }
274});
275```
276
277For more information see examples below.
278
279**controllers** - list of controllers by name that should be injected. Each controller
280is created with a new `$rootScope` instance.
281
282**NOTE: For each created controller, its SCOPE instance will be in the dependencies object.**
283
284```js
285angular.module('D', [])
286 .controller('dController', function ($scope) {
287 $scope.foo = 'foo';
288 });
289ngDescribe({
290 modules: 'D',
291 controllers: 'dController',
292 tests: function (deps) {
293 it('is a scope for controller', function () {
294 expect(typeof deps.dController).toEqual('object');
295 // deps.dController is the $scope object injected into dController
296 expect(deps.dController.foo).toEqual('foo');
297 });
298 }
299});
300```
301
302**element** - HTML fragment string for testing custom directives and DOM updates.
303
304```js
305ngDescribe({
306 element: '<my-foo bar="baz"></my-foo>'
307});
308```
309
310The compiled `angular.element` will be injected into the dependencies object under `element` property.
311See examples below for more information. The compilation will create a new scope object too.
312
313**parentScope** - when creating HTML fragment, copies properties from this object into the
314scope. The returned dependencies object will have `deps.parentScope` that is the new scope.
315
316```js
317// myFoo directive uses isolate scope for example
318ngDescribe({
319 element: '<my-foo bar="baz"></my-foo>',
320 parentScope: {
321 baz: 42
322 },
323 tests: function (deps) {
324 it('baz -> bar', function () {
325 deps.parentScope.baz = 100;
326 deps.$rootScope.$apply();
327 expect(deps.element.isolateScope().bar).toEqual(100);
328 });
329 }
330});
331```
332
333See "2 way binding" example below.
334
335**configs** - object with modules that have provider that can be used to inject
336run time settings.
337See *Update 1* in
338[Inject valid constants into Angular](http://glebbahmutov.com/blog/inject-valid-constants-into-angular/)
339blog post and examples below.
340
341### Secondary options
342
343**verbose** - flag to print debug messages during execution
344
345**only** - flag to run this set of tests and skip the rest. Equivalent to
346[ddescribe or describe.only](http://glebbahmutov.com/blog/focus-on-karma-test/).
347
348```js
349ngDescribe({
350 name: 'run this module only',
351 only: true
352});
353```
354
355**skip** - flag to skip this group of specs. Equivalent to `xdescribe` or `describe.skip`.
356Could be a string message explaining the reason for skipping the spec.
357
358**exposeApi** - expose low-level ngDescribe methods
359
360The `tests` callback will get the second argument, which is an object with the following methods
361
362 {
363 setupElement: function (elementHtml),
364 setupControllers: function (controllerNames)
365 }
366
367You can use `setupElement` to control when to create the element.
368For example, instead of creating element right away, expose element factory so that you can create
369an element *after* running a `beforeEach` block. Useful for setting up mock backend before creating
370an element.
371
372```js
373ngDescribe({
374 exposeApi: true,
375 inject: '$httpBackend',
376 // no element option
377 tests: function (deps, describeApi) {
378 beforeEach(function () {
379 deps.$httpBackend
380 .expectGET('/api/foo/bar').respond(500);
381 });
382 beforeEach(function () {
383 // now create an element ourselves
384 describeApi.setupElement('<study-flags />');
385 });
386 it('created an element', function () {
387 la(check.has(deps.element));
388 });
389 });
390});
391```
392
393See the spec in [test/expose-spec.js](test/expose-spec.js)
394
395Or you can use `setupControllers` to create controller objects AFTER setting up your spies.
396
397```js
398angular.module('BroadcastController', [])
399 .controller('broadcastController', function broadcastController($rootScope) {
400 $rootScope.$broadcast('foo');
401 });
402```
403
404We need to listen for the `foo` broadcast inside a unit test before creating the controller.
405If we let `ngDescribe` create the "broadcastController" it will be too late. Instead we
406can tell the `ngDescribe` to expose the low-level api and then we create the controllers when
407we are ready
408
409```js
410ngDescribe({
411 name: 'spy on controller init',
412 modules: 'BroadcastController',
413 inject: '$rootScope',
414 exposeApi: true,
415 tests: function (deps, describeApi) {
416 it('can catch the broadcast in controller init', function (done) {
417 var heardFoo;
418 deps.$rootScope.$on('foo', function () {
419 heardFoo = true;
420 done();
421 });
422 describeApi.setupControllers('broadcastController');
423 });
424 }
425});
426```
427
428See the spec in [test/controller-init-spec.js](test/controller-init-spec.js)
429
430**http** - shortcut for specifying mock HTTP responses,
431built on top of [$httpBackend](https://docs.angularjs.org/api/ngMock/service/$httpBackend).
432Each GET request will be mapped to `$httpBackend.whenGET` for example. You can provide
433data, response code + data pair, response code + data + headers and optionally statusText
434or custom function to return something using custom logic.
435If you use `http` property, then the injected dependencies will have `http` object that
436you can flush (it is really `$httpBackend` object).
437
438```js
439ngDescribe({
440 inject: '$http', // for making test calls
441 http: {
442 get: {
443 '/my/url': 42, // status 200, data 42
444 '/my/other/url': [202, 42], // status 202, data 42,
445 '/my/smart/url': function (method, url, data, headers) {
446 return [500, 'something is wrong'];
447 } // status 500, data "something is wrong"
448 },
449 post: {
450 '/my/url': '/my/url': [201, {message: 'ok'}, {Location: '/new/url'}, 'this is the new response'], // status data, headers and statusText
451 // same format as GET
452 }
453 },
454 tests: function (deps) {
455 it('responds', function (done) {
456 deps.$http.get('/my/other/url')
457 .then(function (response) {
458 // expect
459 // response.status = 202
460 // response.data = 42
461 done();
462 });
463 deps.http.flush();
464 });
465 }
466});
467```
468All standard methods should be supported (`get`, `head`, `post`, `put`, `delete`, `jsonp` and `patch`).
469
470Each of the methods can return a function that returns an configuration object, see [mock http](#mock-http).
471
472**step** - shortcut for running the digest cycle and mock http flush
473
474```js
475tests: function (deps) {
476 it('runs the digest cycle', function (done) {
477 $q.when(42).finally(done);
478 deps.step();
479 // same as deps.$rootScope.$digest();
480 });
481}
482```
483
484Also flushes the mock http backend
485
486```js
487http: {}
488tests: function (deps) {
489 it('returns expected result', function (done) {
490 deps.$http.get(...)
491 .then(...)
492 .finally(done);
493 deps.step();
494 // same as deps.http.flush();
495 });
496}
497```
498
499**root** - alternative context for BDD callbacks
500
501Imagine we are loading Angular and ngDescribe in a synthetic browser environment (like
502[jsdom](https://www.npmjs.com/package/jsdom)). ngDescribe attaches itself to synthetic `window`
503object, but the test framework callbacks are attached to `global` object, not `window`.
504By passing an alternative object, we allow ngDescribe to discover `it`, `beforeEach`, etc.
505
506```js
507// load ngDescribe in jsdom under Node
508window.ngDescribe({
509 root: global,
510 tests: function (deps) {
511 ...
512 }
513})
514```
515
516See repo [ng-describe-jsdom](https://gitlab.com/bahmutov/ng-describe-jsdom) for actual
517example that tests Angular without a browser, only a synthetic emulation.
518
519
520## Examples
521
522Some examples use Jasmine matchers, others use `la` assertion from
523[lazy-ass](https://github.com/bahmutov/lazy-ass) library and *done* callback argument
524from [Mocha](http://visionmedia.github.io/mocha/) testing framework.
525
526Also, note that the dependencies object is filled **only** inside the unit test callbacks `it` and
527setup helpers `beforeEach` and `afterEach`
528
529```js
530ngDescribe({
531 inject: 'foo',
532 tests: function (deps) {
533 // deps is an empty object here
534 beforeEach(function () {
535 // deps object has 'foo'
536 });
537 // deps is an empty object here
538 it(function () {
539 // deps object has 'foo'
540 });
541 // deps is an empty object here
542 afterEach(function () {
543 // deps object has 'foo'
544 });
545 }
546});
547```
548
549### Test value provided by a module
550
551```js
552// A.js
553angular.module('A', [])
554 .value('foo', 'bar');
555// A-spec.js
556ngDescribe({
557 name: 'test value',
558 modules: 'A',
559 inject: 'foo',
560 tests: function (deps) {
561 // deps object has every injected dependency as a property
562 it('has correct value foo', function () {
563 expect(deps.foo).toEqual('bar');
564 });
565 }
566});
567```
568
569### Test a filter
570
571We can easily test a built-in or custom filter function
572
573```js
574ngDescribe({
575 name: 'built-in filter',
576 inject: '$filter',
577 tests: function (deps) {
578 it('can convert to lowercase', function () {
579 var lowercase = deps.$filter('lowercase');
580 la(lowercase('Foo') === 'foo');
581 });
582 }
583});
584```
585
586### Test a service
587
588We can inject a service to test using the same approach. You can even use multiple specs inside `tests` callback.
589
590```js
591// B.js
592angular.module('B', ['A'])
593 .service('addFoo', function (foo) {
594 return function (str) {
595 return str + foo;
596 };
597 });
598// B-spec.js
599ngDescribe({
600 name: 'service tests',
601 modules: 'B',
602 inject: 'addFoo',
603 tests: function (deps) {
604 it('is a function', function () {
605 expect(typeof deps.addFoo).toEqual('function');
606 });
607 it('appends value of foo to any string', function () {
608 var result = deps.addFoo('x');
609 expect(result).toEqual('xbar');
610 });
611 }
612});
613```
614
615### Test controller and scope
616
617We can easily create instances of controller functions and scope objects.
618In this example we also inject `$timeout` service to speed up delayed actions
619(see [Testing Angular async stuff](http://glebbahmutov.com/blog/testing-angular-async-stuff/)).
620
621```js
622angular.module('S', [])
623 .controller('sample', function ($timeout, $scope) {
624 $scope.foo = 'foo';
625 $scope.update = function () {
626 $timeout(function () {
627 $scope.foo = 'bar';
628 }, 1000);
629 };
630 });
631ngDescribe({
632 name: 'timeout in controller',
633 modules: 'S',
634 // inject $timeout so we can flush the timeout queue
635 inject: ['$timeout'],
636 controllers: 'sample',
637 tests: function (deps) {
638 // deps.sample = $scope object injected into sample controller
639 it('has initial values', function () {
640 la(deps.sample.foo === 'foo');
641 });
642 it('updates after timeout', function () {
643 deps.sample.update();
644 deps.$timeout.flush();
645 la(deps.sample.foo === 'bar');
646 });
647 }
648});
649```
650
651### Test directive
652
653```js
654angular.module('MyFoo', [])
655 .directive('myFoo', function () {
656 return {
657 restrict: 'E',
658 replace: true,
659 template: '<span>{{ bar }}</span>'
660 };
661 });
662ngDescribe({
663 name: 'MyFoo directive',
664 modules: 'MyFoo',
665 element: '<my-foo></my-foo>',
666 tests: function (deps) {
667 it('can update DOM using binding', function () {
668 la(check.has(deps, 'element'), 'has compiled element');
669 var scope = deps.element.scope();
670 scope.bar = 'bar';
671 scope.$apply();
672 la(deps.element.html() === 'bar');
673 });
674 }
675});
676```
677
678### Test controllerAs syntax
679
680If you use `controllerAs` syntax without any components (see [Binding to ...][binding] post or
681[Separate ...][separate]), then you can still test it quickly
682
683```js
684angular.module('H', [])
685 .controller('hController', function () {
686 // notice we attach properties to the instance, not to the $scope
687 this.foo = 'foo';
688 });
689 ngDescribe({
690 module: 'H',
691 element: '<div ng-controller="hController as ctrl">{{ ctrl.foo }}</div>',
692 tests: function (deps) {
693 it('created controller correctly', function () {
694 var compiledHtml = deps.element.html();
695 // 'foo'
696 });
697 it('changes value', function () {
698 var ctrl = deps.element.controller();
699 // { foo: 'foo' }
700 ctrl.foo = 'bar';
701 deps.element.scope().$apply();
702 var compiledHtml = deps.element.html();
703 // 'bar'
704 });
705 }
706 });
707```
708
709[binding]: http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html
710[separate]: http://glebbahmutov.com/blog/separate-model-from-view-in-angular/
711
712### Test controller instance in custom directive
713
714If you add methods to the controller inside custom directive, use `controllerAs` syntax to
715expose the controller instance.
716
717```js
718angular.module('C', [])
719 .directive('cDirective', function () {
720 return {
721 controllerAs: 'ctrl', // puts controller instance onto scope as ctrl
722 controller: function ($scope) {
723 $scope.foo = 'foo';
724 this.foo = function getFoo() {
725 return $scope.foo;
726 };
727 }
728 };
729 });
730ngDescribe({
731 name: 'controller for directive instance',
732 modules: 'C',
733 element: '<c-directive></c-directive>',
734 tests: function (deps) {
735 it('has controller', function () {
736 var scope = deps.element.scope(); // grabs scope
737 var controller = scope.ctrl; // grabs controller instance
738 la(typeof controller.foo === 'function');
739 la(controller.foo() === 'foo');
740 scope.foo = 'bar';
741 la(controller.foo() === 'bar');
742 });
743 }
744});
745```
746
747### Test 2 way binding
748
749If a directive implements isolate scope, we can configure parent scope separately.
750
751```js
752angular.module('IsolateFoo', [])
753 .directive('aFoo', function () {
754 return {
755 restrict: 'E',
756 replace: true,
757 scope: {
758 bar: '='
759 },
760 template: '<span>{{ bar }}</span>'
761 };
762 });
763```
764
765We can use `element` together with `parentScope` property to set initial values.
766
767```js
768ngDescribe({
769 modules: 'IsolateFoo',
770 element: '<a-foo bar="x"></a-foo>',
771 parentScope: {
772 x: 'initial'
773 },
774 tests: function (deps) {
775 it('has correct initial value', function () {
776 var scope = deps.element.isolateScope();
777 expect(scope.bar).toEqual('initial');
778 });
779 }
780});
781```
782
783We can change parent's values to observe propagation into the directive
784
785```js
786// same setup
787it('updates isolate scope', function () {
788 deps.parentScope.x = 42;
789 deps.$rootScope.$apply();
790 var scope = deps.element.isolateScope();
791 expect(scope.bar).toEqual(42);
792});
793```
794
795### beforeEach and afterEach
796
797You can use multiple `beforeEach` and `afterEach` inside `tests` function.
798
799```js
800ngDescribe({
801 name: 'before and after example',
802 modules: ['A'],
803 inject: ['foo'],
804 tests: function (deps) {
805 var localFoo;
806 beforeEach(function () {
807 // dependencies are already injected
808 la(deps.foo === 'bar');
809 localFoo = deps.foo;
810 });
811 it('has correct value foo', function () {
812 la(localFoo === 'bar');
813 });
814 afterEach(function () {
815 la(localFoo === 'bar');
816 // dependencies are still available
817 la(deps.foo === 'bar');
818 });
819 }
820});
821```
822
823This could be useful for setting up additional mocks, like `$httpBackend`.
824
825```js
826angular.module('apiCaller', [])
827 .service('getIt', function ($http) {
828 return function () {
829 return $http.get('/my/url');
830 };
831 });
832ngDescribe({
833 name: 'http mock backend example',
834 modules: ['apiCaller'],
835 inject: ['getIt', '$httpBackend'],
836 tests: function (deps) {
837 beforeEach(function () {
838 deps.$httpBackend.expectGET('/my/url').respond(200, 42);
839 });
840 it('returns result from server', function (done) {
841 deps.getIt().then(function (response) {
842 la(response && response.status === 200);
843 la(response.data === 42);
844 done();
845 });
846 deps.$httpBackend.flush();
847 });
848 afterEach(function () {
849 deps.$httpBackend.verifyNoOutstandingRequest();
850 deps.$httpBackend.verifyNoOutstandingExpectation();
851 });
852 }
853});
854```
855
856**Note** if you use `beforeEach` block with `element`, the `beforeEach` runs *before* the element
857is created. This gives you a chance to setup mocks before running the element and possibly making calls.
858If you really want to control when an element is created use `exposeApi` option
859(see [Secondary options](#secondary-options)).
860
861### Mocking
862
863#### Mock value provided by a module
864
865Often during testing we need to mock something provided by a module, even if it is
866passed via dependency injection. ng-describe makes it very simple. List all modules with values
867to be mocked in `mocks` object property.
868
869```js
870// C.js
871angular.module('C', ['A'])
872 .service('getFoo', function (foo) {
873 // foo is provided by module A
874 return function getFoo() {
875 return foo;
876 };
877 });
878// C-spec.js
879ngDescribe({
880 name: 'test C with mocking top level',
881 modules: ['C'],
882 inject: ['getFoo'],
883 mocks: {
884 // replace C.getFoo with mock function that returns 11
885 C: {
886 getFoo: function () {
887 return 11;
888 }
889 }
890 },
891 verbose: false,
892 tests: function (deps) {
893 it('has mock injected value', function () {
894 var result = deps.getFoo();
895 la(result === 11, 'we got back mock value', result);
896 });
897 }
898});
899```
900
901Remember when making mocks, it is always `module name : provider name : mocked property name`
902
903```js
904mocks: {
905 'module name': {
906 'mocked provider name': {
907 'mocked value name'
908 }
909 }
910}
911```
912
913Note: the mocked values are injected using `$provider.constant` call to be able to override both
914values and constants
915
916```js
917angular.module('A10', [])
918 .constant('foo', 'bar');
919ngDescribe({
920 modules: 'A10',
921 mock: {
922 A10: {
923 foo: 42
924 }
925 },
926 inject: 'foo',
927 tests: function (deps) {
928 it('has correct constant foo', function () {
929 expect(deps.foo).toEqual(42);
930 });
931 }
932});
933```
934
935You can even mock part of the module itself and use mock value in other parts via injection
936
937```js
938angular.module('LargeModule', [])
939 .constant('foo', 'foo')
940 .service('getFoo', function (foo) {
941 return function getFoo() {
942 return foo;
943 };
944 });
945ngDescribe({
946 name: 'mocking part of the module itself',
947 modules: 'LargeModule',
948 inject: 'getFoo',
949 mock: {
950 LargeModule: {
951 foo: 'bar'
952 }
953 },
954 tests: function (deps) {
955 it('service injects mock value', function () {
956 la(deps.getFoo() === 'bar', 'returns mock value');
957 });
958 }
959});
960```
961
962#### Angular services inside mocks
963
964You can use other injected dependencies inside mocked functions, using
965injected values and free parameters.
966
967```js
968ngDescribe({
969 inject: ['getFoo', '$rootScope'],
970 mocks: {
971 C: {
972 // use angular $q service in the mock function
973 // argument "value" remains free
974 getFoo: function ($q, value) {
975 return $q.when(value);
976 }
977 }
978 },
979 tests: function (deps) {
980 it('injected $q into mock', function (done) {
981 deps.getFoo('foo').then(function (result) {
982 expect(result).toEqual('foo');
983 done();
984 });
985 deps.$rootScope.$apply(); // resolve promise
986 });
987 }
988});
989```
990
991#### Mock $http.get
992
993Often we need some dummy response from `$http.get` method. We can use mock `httpBackend`
994or mock the `$http` object. For example to always return mock value when making any GET request,
995we can use
996
997```js
998mocks: {
999 ng: {
1000 $http: {
1001 get: function ($q, url) {
1002 // inspect url if needed
1003 return $q.when({
1004 data: {
1005 life: 42
1006 }
1007 });
1008 }
1009 }
1010 }
1011}
1012```
1013
1014`$http` service returns a promise that resolves with a *response* object. The actual result to send
1015is placed into the `data` property, as I show here.
1016
1017#### Mock http responses
1018
1019You can use a shortcut to define mock HTTP responses via `$httpBackend` module. For example,
1020you can define static responses.
1021
1022```js
1023ngDescribe({
1024 http: {
1025 get: {
1026 '/some/url': 42,
1027 '/some/other/url': [500, 'something went wrong']
1028 },
1029 post: {
1030 // you can use custom functions too
1031 '/some/post/url': function (method, url, data, headers) {
1032 return [200, 'ok'];
1033 }
1034 }
1035 }
1036});
1037```
1038All HTTP methods are supported (`get`, `post`, `delete`, `put`, etc.).
1039
1040You can also get a function that would return a config object.
1041
1042```js
1043var mockGetApi = {
1044 '/some/url': 42
1045};
1046mockGetApi['/some/other/url'] = [500, 'not ok'];
1047ngDescribe({
1048 http: {
1049 get: mockGetApi
1050 }
1051});
1052```
1053
1054You can use `deps.http.flush()` to move the http responses along.
1055
1056You can return the entire http mock object from a function, or combine objects with functions.
1057
1058```js
1059function constructMockApi() {
1060 return {
1061 get: function () {
1062 return { '/my/url': 42 };
1063 },
1064 post: {
1065 '/my/other/url': [200, 'nice']
1066 }
1067 };
1068}
1069ngDescribe({
1070 http: constructMockApi,
1071 test: function (deps) {
1072 ...
1073 }
1074});
1075```
1076
1077You can use exact query arguments too
1078
1079```js
1080http: {
1081 get: {
1082 '/foo/bar?search=value': 42,
1083 '/foo/bar?search=value&something=else': 'foo'
1084 }
1085}
1086// $http.get('/foo/bar?search=value') will resolve with value 42
1087// $http.get('/foo/bar?search=value&something=else') will resolve with value 'foo'
1088```
1089
1090or you can build the query string automatically by passing `params` property in the request config
1091objet
1092
1093```js
1094http: {
1095 get: {
1096 '/foo/bar?search=value&something=else': 'foo'
1097 }
1098}
1099// inside the unit test
1100var config = {
1101 params: {
1102 search: 'value',
1103 something: 'else'
1104 }
1105};
1106$http.get('/foo/bar', config).then(function (response) {
1107 // response.data = 'foo'
1108});
1109```
1110
1111**note** the `http` mocks are defined using `$httpBack.when(method, ...)` calls,
1112which are looser than `$httpBackend.expect(method, ...)`,
1113see [ngMock/$httpBackend](https://docs.angularjs.org/api/ngMock/service/$httpBackend).
1114
1115### Spying
1116
1117#### Spy on injected methods
1118
1119One can quickly spy on injected services (or other methods) using [sinon.js](http://sinonjs.org/)
1120similarly to [spying on the regular JavaScript methods](http://glebbahmutov.com/blog/spying-on-methods/).
1121
1122* Include a browser-compatible combined [sinon.js build](http://sinonjs.org/releases/sinon-1.12.1.js)
1123into the list of loaded Karma files.
1124* Setup spy in the `beforeEach` function. Since every injected service is a method on the `deps`
1125object, the setup is a single command.
1126* Restore the original method in `afterEach` function.
1127
1128```js
1129// source code
1130angular.module('Tweets', [])
1131 .service('getTweets', function () {
1132 return function getTweets(username) {
1133 console.log('returning # of tweets for', username);
1134 return 42;
1135 };
1136 });
1137```
1138
1139```js
1140// spec
1141ngDescribe({
1142 name: 'spying on Tweets getTweets service',
1143 modules: 'Tweets',
1144 inject: 'getTweets',
1145 tests: function (deps) {
1146 beforeEach(function () {
1147 sinon.spy(deps, 'getTweets');
1148 });
1149 afterEach(function () {
1150 deps.getTweets.restore();
1151 });
1152 it('calls getTweets service', function () {
1153 var n = deps.getTweets('foo');
1154 la(n === 42, 'resolved with correct value');
1155 la(deps.getTweets.called, 'getTweets was called (spied using sinon)');
1156 la(deps.getTweets.firstCall.calledWith('foo'));
1157 });
1158 }
1159});
1160```
1161
1162#### Spy on injected function
1163
1164You can inject a function, but use a [Sinon spy](http://sinonjs.org/docs/#spies) instead
1165of the injected function to get additional information. For example, to spy on the `$filter uppercase`,
1166we can use the following code.
1167
1168```js
1169ngDescribe({
1170 name: 'spying on a filter',
1171 inject: '$filter',
1172 tests: function (deps) {
1173 /*
1174 to spy on a injected filter, need to grab the actual filter function
1175 and then create a spy
1176 */
1177 // _uppercase = angular uppercase $filter
1178 // uppercase = spy on the _uppercase
1179 var _uppercase, uppercase;
1180 beforeEach(function () {
1181 _uppercase = deps.$filter('uppercase');
1182 uppercase = sinon.spy(_uppercase);
1183 });
1184 it('converts string to uppercase', function () {
1185 var result = uppercase('foo');
1186 la(result === 'FOO', 'converted string to uppercase', result);
1187 la(uppercase.calledOnce, 'uppercase was called once');
1188 la(uppercase.calledWith('foo'));
1189 });
1190 }
1191});
1192```
1193
1194#### Spy on 3rd party service injected some place else
1195
1196Let us say you need to verify that the `$interval` service injected in the module under test
1197was called. It is a little verbose to verify from the unit test. We must mock the `$interval`
1198with our function and then call the actual `$interval` from the module `ng` to provide the
1199same functionality.
1200
1201Source code we are trying to unit test
1202
1203```js
1204angular.module('IntervalExample', [])
1205 .service('numbers', function ($interval, $rootScope) {
1206 return function emitNumbers(delay, n) {
1207 var k = 0;
1208 $interval(function () {
1209 $rootScope.$emit('number', k);
1210 k += 1;
1211 }, 100, n);
1212 };
1213 });
1214```
1215
1216In the unit test we will mock `$interval` service for module `IntervalExample`
1217
1218```js
1219// unit test start
1220var intervalCalled;
1221ngDescribe({
1222 name: 'spying on $interval',
1223 module: 'IntervalExample',
1224 inject: ['numbers', '$rootScope'],
1225 verbose: false,
1226 only: false,
1227 mocks: {
1228 IntervalExample: {
1229 $interval: function mockInterval(fn, delay, n) {
1230 var injector = angular.injector(['ng']);
1231 var $interval = injector.get('$interval');
1232 intervalCalled = true;
1233 return $interval(fn, delay, n);
1234 }
1235 }
1236 },
1237 tests: function (deps) {
1238 // unit test goes here
1239 }
1240});
1241```
1242
1243A unit test just calls the `numbers` function and then checks the variable `intervalCalled`
1244
1245```js
1246it('emits 3 numbers', function (done) {
1247 deps.$rootScope.$on('number', function (event, k) {
1248 if (k === 2) {
1249 done();
1250 }
1251 });
1252 // emit 3 numbers with 100ms interval
1253 deps.numbers(100, 3);
1254 la(intervalCalled, 'the $interval was called somewhere');
1255});
1256```
1257
1258You can see the unit test in file [test/spying-on-interval-spec.js](test/spying-on-interval-spec.js).
1259
1260#### Spy on mocked service
1261
1262If we mock an injected service, we can still spy on it, just like as if we were spying on the
1263regular service. For example, let us take the same method as above and mock it.
1264
1265```js
1266angular.module('Tweets', [])
1267 .service('getTweets', function () {
1268 return function getTweets(username) {
1269 return 42;
1270 };
1271 });
1272```
1273
1274The mock will return a different number.
1275
1276```js
1277ngDescribe({
1278 name: 'spying on mock methods',
1279 inject: 'getTweets',
1280 mocks: {
1281 Tweets: {
1282 getTweets: function (username) {
1283 return 1000;
1284 }
1285 }
1286 },
1287 tests: function (deps) {
1288 beforeEach(function () {
1289 sinon.spy(deps, 'getTweets');
1290 });
1291 afterEach(function () {
1292 deps.getTweets.restore();
1293 });
1294 it('calls mocked getTweets service', function () {
1295 var n = deps.getTweets('bar');
1296 la(n === 1000, 'resolved with correct value from the mock service');
1297 la(deps.getTweets.called,
1298 'mock service getTweets was called (spied using sinon)');
1299 la(deps.getTweets.firstCall.calledWith('bar'),
1300 'mock service getTweets was called with expected argument');
1301 });
1302 }
1303});
1304```
1305
1306### Configure module
1307
1308If you use a separate module with namesake provider to pass configuration into the modules
1309(see [Inject valid constants into Angular](http://glebbahmutov.com/blog/inject-valid-constants-into-angular/)),
1310you can easily configure these modules.
1311
1312```js
1313angular.module('App', ['AppConfig'])
1314 .service('foo', function (AppConfig) {
1315 return function foo() {
1316 return GConfig.bar;
1317 };
1318 });
1319// config module has provider with same name
1320angular.module('AppConfig', [])
1321 .provider('AppConfig', function () {
1322 var config = {};
1323 return {
1324 set: function (settings) {
1325 config = settings;
1326 },
1327 $get: function () {
1328 return config;
1329 }
1330 };
1331 });
1332// spec file
1333ngDescribe({
1334 name: 'config module example',
1335 modules: 'App',
1336 inject: 'foo',
1337 configs: {
1338 // every config module will be loaded automatically
1339 AppConfig: {
1340 bar: 'boo!'
1341 }
1342 },
1343 tests: function (deps) {
1344 it('foo has configured bar value', function () {
1345 expect(deps.foo()).toEqual('boo!');
1346 });
1347 }
1348});
1349```
1350
1351You can configure multiple modules at the same time. Note that during the configuration
1352Angular is yet to be loaded. Thus you cannot use Angular services inside the configuration blocks.
1353
1354### Helpful failure messages
1355
1356ng-describe works inside [helpDescribe function](https://github.com/bahmutov/lazy-ass-helpful#lazy-ass-helpful-bdd),
1357producing meaningful error messages on failure (if you use [lazy assertions](https://github.com/bahmutov/lazy-ass)).
1358
1359```js
1360helpDescribe('ngDescribe inside helpful', function () {
1361 ngDescribe({
1362 name: 'example',
1363 tests: function () {
1364 it('gives helpful error message', function () {
1365 var foo = 2, bar = 3;
1366 la(foo + bar === 4); // wrong on purpose
1367 });
1368 }
1369 });
1370});
1371```
1372when this test fails, it generates meaningful message with all relevant information: the expression
1373that fails `foo + bar === 4` and runtime values of `foo` and `bar`.
1374
1375 PhantomJS 1.9.7 (Mac OS X)
1376 ட ngDescribe inside helpful
1377 ட example
1378 ட ✘ gives helpful error message FAILED
1379 Error: condition [foo + bar === 4] foo: 2 bar: 3
1380 at lazyAss (/ng-describe/node_modules/lazy-ass/index.js:57)
1381 PhantomJS 1.9.7 (Mac OS X): Executed 37 of 38 (1 FAILED) (skipped 1) (0.053 secs / 0.002 secs)
1382
1383
1384## Development
1385
1386To build the README document, run unit tests and linter
1387
1388 npm run build
1389
1390To run all unit tests (against different Angular versions)
1391
1392 npm test
1393
1394To keep a watch and rerun build + lint + tests on source file change
1395
1396 npm run watch
1397
1398For now, all source is in a single `ng-describe.js` file, while the documentation
1399is generated from Markdown files in the `docs` folder
1400
1401To just run karma unit tests via Grunt plugin
1402
1403 npm run karma
1404
1405If you have Karma runner installed globally you can run all the unit tests yourself ones
1406
1407 karma start --single-run=true test/karma.conf.js
1408
1409### Updating dependencies
1410
1411This project uses a lot of 3rd party dependencies that constantly get out of date.
1412To reliably update dependencies to the latest working versions, we use
1413[next-update](https://github.com/bahmutov/next-update). There is already a script command
1414
1415 npm run update-dependencies
1416
1417You can upgrade a particular dependency by adding "-m <name>", for example
1418
1419 npm run update-dependencies -- -m jscs
1420
1421If you use [npm-quick-run](https://github.com/bahmutov/npm-quick-run) you can use shorthand
1422
1423 nr u -m jscs
1424
1425
1426
1427## Modules used
1428* [check-more-types](https://github.com/kensho/check-more-types) - Large collection of predicates.
1429* [lazy-ass](https://github.com/bahmutov/lazy-ass) - Lazy assertions without performance penalty
1430
1431
1432## License
1433
1434Author: Kensho &copy; 2014
1435
1436* [@kensho](https://twitter.com/kensho)
1437* [kensho.com](http://kensho.com)
1438
1439Support: if you find any problems with this library,
1440[open issue](https://github.com/kensho/ng-describe/issues) on Github
1441
1442
1443The MIT License (MIT)
1444
1445Copyright (c) 2014 Kensho
1446
1447Permission is hereby granted, free of charge, to any person obtaining a copy of
1448this software and associated documentation files (the "Software"), to deal in
1449the Software without restriction, including without limitation the rights to
1450use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
1451the Software, and to permit persons to whom the Software is furnished to do so,
1452subject to the following conditions:
1453
1454The above copyright notice and this permission notice shall be included in all
1455copies or substantial portions of the Software.
1456
1457THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1458IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
1459FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
1460COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
1461IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
1462CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1463
1464
1465