1 | # ng-describe v0.14.1
|
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 | [![Coverage Status][ng-describe-coverage-image] ][ng-describe-coverage-url]
|
10 | [![Codacy Badge][ng-describe-codacy-image] ][ng-describe-codacy-url]
|
11 | [![dependencies][ng-describe-dependencies-image] ][ng-describe-dependencies-url]
|
12 | [![devdependencies][ng-describe-devdependencies-image] ][ng-describe-devdependencies-url]
|
13 |
|
14 | Tested against angular v1.2, v1.3 and v1.4
|
15 |
|
16 | Dependent projects tested using [dont-break][dont-break] - [![Circle CI] [circle-icon] ][circle-url]
|
17 |
|
18 | Join [Kensho](https://kensho.com/#/careers) and change the way financial industry analyzes information.
|
19 | We love open source and use a bleeding edge technology stack.
|
20 |
|
21 | [ng-describe-icon]: https://nodei.co/npm/ng-describe.png?downloads=true
|
22 | [ng-describe-url]: https://npmjs.org/package/ng-describe
|
23 | [ng-describe-ci-image]: https://travis-ci.org/kensho/ng-describe.png?branch=master
|
24 | [ng-describe-ci-url]: https://travis-ci.org/kensho/ng-describe
|
25 | [ng-describe-coverage-image]: https://coveralls.io/repos/kensho/ng-describe/badge.png
|
26 | [ng-describe-coverage-url]: https://coveralls.io/r/kensho/ng-describe
|
27 | [ng-describe-dependencies-image]: https://david-dm.org/kensho/ng-describe.png
|
28 | [ng-describe-dependencies-url]: https://david-dm.org/kensho/ng-describe
|
29 | [ng-describe-devdependencies-image]: https://david-dm.org/kensho/ng-describe/dev-status.png
|
30 | [ng-describe-devdependencies-url]: https://david-dm.org/kensho/ng-describe#info=devDependencies
|
31 | [ng-describe-codacy-image]: https://www.codacy.com/project/badge/25cb5d1410c7497cb057d887d1f3ea23
|
32 | [ng-describe-codacy-url]: https://www.codacy.com/public/kensho/ng-describe.git
|
33 |
|
34 | [quality-badge]: http://npm.packagequality.com/badge/ng-describe.png
|
35 | [quality-url]: http://packagequality.com/#?package=ng-describe
|
36 |
|
37 | [circle-icon]: https://circleci.com/gh/kensho/ng-describe.svg?style=svg
|
38 | [circle-url]: https://circleci.com/gh/kensho/ng-describe
|
39 | [dont-break]: https://github.com/bahmutov/dont-break
|
40 |
|
41 |
|
42 |
|
43 | * [Intro](#intro)
|
44 | * [Install](#install)
|
45 | * [API](#api)
|
46 | * [Primary options](#primary-options)
|
47 | * [Secondary options](#secondary-options)
|
48 | * [Examples](#examples)
|
49 | * [Test value provided by a module](#test-value-provided-by-a-module)
|
50 | * [Test a filter](#test-a-filter)
|
51 | * [Test a service](#test-a-service)
|
52 | * [Test controller and scope](#test-controller-and-scope)
|
53 | * [Test directive](#test-directive)
|
54 | * [Test controllerAs syntax](#test-controlleras-syntax)
|
55 | * [Test controller instance in custom directive](#test-controller-instance-in-custom-directive)
|
56 | * [Test 2 way binding](#test-2-way-binding)
|
57 | * [Mock value provided by a module](#mock-value-provided-by-a-module)
|
58 | * [Angular services inside mocks](#angular-services-inside-mocks)
|
59 | * [Mock $http.get](#mock-httpget)
|
60 | * [mock http](#mock-http)
|
61 | * [beforeEach and afterEach](#beforeeach-and-aftereach)
|
62 | * [Spy on injected methods](#spy-on-injected-methods)
|
63 | * [Spy on mocked service](#spy-on-mocked-service)
|
64 | * [Configure module](#configure-module)
|
65 | * [Helpful failure messages](#helpful-failure-messages)
|
66 | * [License](#license)
|
67 |
|
68 |
|
69 | ## Intro
|
70 |
|
71 | Unit testing and mocking AngularJs requires a lot of boilerplate code:
|
72 | ```js
|
73 | describe('typical test', function () {
|
74 | var $rootScope, foo;
|
75 | beforeEach(function () {
|
76 | angular.mock.module('A');
|
77 | // other modules
|
78 | });
|
79 | beforeEach(inject(function (_$rootScope_, _foo_) {
|
80 | $rootScope = _$rootScope_;
|
81 | foo = _foo_;
|
82 | }));
|
83 | it('finally a test', function () {
|
84 | $rootScope.$apply(); // for example
|
85 | expect(foo).toEqual('bar');
|
86 | });
|
87 | });
|
88 | ```
|
89 |
|
90 | ng-describe makes testing simple modules a breeze.
|
91 | Just list which modules you would like to load, which values / services / etc.
|
92 | you would like to inject and then start testing. Same test as above using ng-describe
|
93 | is much shorter and clearer:
|
94 | ```js
|
95 | ngDescribe({
|
96 | modules: 'A',
|
97 | inject: ['$rootScope', 'foo'],
|
98 | tests: function (deps) {
|
99 | it('finally a test', function () {
|
100 | deps.$rootScope.$apply();
|
101 | expect(deps.foo).toEqual('bar');
|
102 | });
|
103 | });
|
104 | });
|
105 | ```
|
106 | ng-describe can inject dependencies, mock modules, set configs, create controllers, scopes, and
|
107 | even html fragments. For more details, continue reading.
|
108 |
|
109 |
|
110 | ## Install
|
111 |
|
112 | `npm install ng-describe --save-dev`
|
113 |
|
114 | Load ng-describe.js after angular, [lazy-ass](https://github.com/bahmutov/lazy-ass),
|
115 | [check-types](https://github.com/philbooth/check-types.js),
|
116 | [check-more-types](https://github.com/kensho/check-more-types) but before your code, for example in Karma conf file
|
117 |
|
118 | npm install lazy-ass check-types check-more-types angular angular-mocks --save-dev
|
119 |
|
120 | // karma.conf.js
|
121 | files: [
|
122 | 'node_modules/check-types/src/check-types.js',
|
123 | 'node_modules/check-more-types/check-more-types.js',
|
124 | 'node_modules/lazy-ass/index.js',
|
125 | 'node_modules/angular/angular.js',
|
126 | 'node_modules/angular-mocks/angular-mocks.js',
|
127 | 'node_modules/ng-describe/ng-describe.js',
|
128 | '<your source.js>',
|
129 | '<your specs.js>'
|
130 | ],
|
131 |
|
132 |
|
133 | ## API
|
134 |
|
135 | ng-describe provides a single function `ngDescribe` that takes an options object.
|
136 |
|
137 | ```js
|
138 | ngDescribe({
|
139 | // your options
|
140 | });
|
141 | ```
|
142 |
|
143 | You do not have to specify every option, there are reasonable defaults. We also tried to make
|
144 | the API [user-friendly](http://glebbahmutov.com/blog/user-friendly-api/).
|
145 |
|
146 | ### Primary options
|
147 |
|
148 | **name** - a string name for the spec, similar to BDD `describe(name, ...)`
|
149 |
|
150 | **modules** - list of modules to inject
|
151 |
|
152 | ```js
|
153 | angular.module('A', []);
|
154 | angular.module('B', []);
|
155 | ngDescribe({
|
156 | name: 'modules example',
|
157 | modules: ['A', 'B']
|
158 | });
|
159 | ```
|
160 |
|
161 | You if have a single module to inject, you can just use a string name without Array notation
|
162 |
|
163 | ```js
|
164 | ngDescribe({
|
165 | name: 'single module',
|
166 | modules: 'A'
|
167 | });
|
168 | ```
|
169 |
|
170 | **inject** - list of dependencies to inject into unit tests. A single dependency can be just a string
|
171 | without Array notation. All dependencies will be exposed as properties of the `deps` argument to the
|
172 | tests callback
|
173 |
|
174 | ```js
|
175 | angular.module('A', []).value('foo', 42);
|
176 | ngDescribe({
|
177 | name: 'inject example',
|
178 | modules: 'A',
|
179 | inject: ['foo', '$timeout'],
|
180 | tests: function (deps) {
|
181 | it('has foo', function () {
|
182 | expect(deps.foo).toEqual(42);
|
183 | });
|
184 | it('has timeout service', function () {
|
185 | expect(typeof deps.$timeout).toEqual('function');
|
186 | });
|
187 | }
|
188 | });
|
189 | ```
|
190 |
|
191 | **tests** - callback function that contains actual specs. This of this as `describe` equivalent with
|
192 | all necessary Angular dependencies taken care of.
|
193 |
|
194 | ```js
|
195 | ngDescribe({
|
196 | inject: ['$q', '$rootScope'],
|
197 | tests: function (deps) {
|
198 | it('injects $q', function () {
|
199 | expect(typeof deps.$q).toEqual('function');
|
200 | });
|
201 | it('can be resolved', function () {
|
202 | deps.$q.when(42).then(function (value) {
|
203 | expect(value).toEqual(42);
|
204 | });
|
205 | // move promises along
|
206 | deps.$rootScope.$digest();
|
207 | });
|
208 | }
|
209 | });
|
210 | ```
|
211 |
|
212 | **mocks** - top level mocks to be substituted into the tests.
|
213 | The mocks override *any* injected dependencies among modules.
|
214 |
|
215 | ```js
|
216 | ngDescribe({
|
217 | mocks: {
|
218 | // each module to mock by name
|
219 | moduleName1: {
|
220 | // each dependency from moduleName1 to mock
|
221 | dependencyName1: mockValue1,
|
222 | dependencyName2: mockValue2
|
223 | // the rest of moduleName1 is unchanged
|
224 | },
|
225 | moduleName2: {
|
226 | // dependencies to mock in moduleName2
|
227 | }
|
228 | }
|
229 | });
|
230 | ```
|
231 |
|
232 | For more information see examples below.
|
233 |
|
234 | **controllers** - list of controllers by name that be injected. Each controller
|
235 | is created with a new `$rootScope` instance.
|
236 |
|
237 | **NOTE: For each created controller, its SCOPE instance will be in the dependencies object.**
|
238 |
|
239 | ```js
|
240 | angular.module('D', [])
|
241 | .controller('dController', function ($scope) {
|
242 | $scope.foo = 'foo';
|
243 | });
|
244 | ngDescribe({
|
245 | modules: 'D',
|
246 | controllers: 'dController',
|
247 | tests: function (deps) {
|
248 | it('is a scope for controller', function () {
|
249 | expect(typeof deps.dController).toEqual('object');
|
250 | // deps.dController is the $scope object injected into dController
|
251 | expect(deps.dController.foo).toEqual('foo');
|
252 | });
|
253 | }
|
254 | });
|
255 | ```
|
256 |
|
257 | **element** - HTML fragment string for testing custom directives and DOM updates.
|
258 |
|
259 | ```js
|
260 | ngDescribe({
|
261 | element: '<my-foo bar="baz"></my-foo>'
|
262 | });
|
263 | ```
|
264 |
|
265 | The compiled `angular.element` will be injected into dependencies object under `element` property.
|
266 | See examples below for more information. The compilation will create a new scope object too.
|
267 |
|
268 | **parentScope** - when creating HTML fragment, copies properties from this object into the
|
269 | scope. The returned dependencies object will have `deps.parentScope` that is the new scope.
|
270 |
|
271 | ```js
|
272 | // myFoo directive uses isolate scope for example
|
273 | ngDescribe({
|
274 | element: '<my-foo bar="baz"></my-foo>',
|
275 | parentScope: {
|
276 | baz: 42
|
277 | },
|
278 | tests: function (deps) {
|
279 | it('baz -> bar', function () {
|
280 | deps.parentScope.baz = 100;
|
281 | deps.$rootScope.$apply();
|
282 | expect(deps.element.isolateScope().bar).toEqual(100);
|
283 | });
|
284 | }
|
285 | });
|
286 | ```
|
287 |
|
288 | See "2 way binding" example below.
|
289 |
|
290 | **configs** - object with modules that have provider that can be used to inject
|
291 | run time settings.
|
292 | See *Update 1* in
|
293 | [Inject valid constants into Angular](http://glebbahmutov.com/blog/inject-valid-constants-into-angular/)
|
294 | blog post and examples below.
|
295 |
|
296 | ### Secondary options
|
297 |
|
298 | **verbose** - flag to print debug messages during execution
|
299 |
|
300 | **only** - flag to run this set of tests and skip the rest. Equivalent to
|
301 | [ddescribe or describe.only](http://glebbahmutov.com/blog/focus-on-karma-test/).
|
302 |
|
303 | ```js
|
304 | ngDescribe({
|
305 | name: 'run this module only',
|
306 | only: true
|
307 | });
|
308 | ```
|
309 |
|
310 | **skip** - flag to skip this group of specs. Equivalent to `xdescribe` or `describe.skip`.
|
311 | Could be a string message explaining the reason for skipping the spec.
|
312 |
|
313 | **exposeApi** - instead of creating element right away, expose element factory so that you can create
|
314 | an element *after* running a `beforeEach` block. Useful for setting up mock backend before creating
|
315 | an element.
|
316 |
|
317 | ```js
|
318 | ngDescribe({
|
319 | exposeApi: true,
|
320 | inject: '$httpBackend',
|
321 | // no element option
|
322 | tests: function (deps, describeApi) {
|
323 | beforeEach(function () {
|
324 | deps.$httpBackend
|
325 | .expectGET('/api/foo/bar').respond(500);
|
326 | });
|
327 | beforeEach(function () {
|
328 | // now create an element ourselves
|
329 | describeApi.setupElement('<study-flags />');
|
330 | });
|
331 | it('created an element', function () {
|
332 | la(check.has(deps.element));
|
333 | });
|
334 | });
|
335 | });
|
336 | ```
|
337 |
|
338 | **http** - shortcut for specifying mock HTTP responses,
|
339 | built on top of [$httpBackend](https://docs.angularjs.org/api/ngMock/service/$httpBackend).
|
340 | Each GET request will be mapped to `$httpBackend.whenGET` for example. You can provide
|
341 | data, response code + data pair or custom function to return something using custom logic.
|
342 | If you use `http` property, then the injected dependencies will have `http` object that
|
343 | you can flush (it is really `$httpBackend` object).
|
344 |
|
345 | ```js
|
346 | ngDescribe({
|
347 | inject: '$http', // for making test calls
|
348 | http: {
|
349 | get: {
|
350 | '/my/url': 42, // status 200, data 42
|
351 | '/my/other/url': [202, 42], // status 202, data 42,
|
352 | '/my/smart/url': function (method, url, data, headers) {
|
353 | return [500, 'something is wrong'];
|
354 | } // status 500, data "something is wrong"
|
355 | },
|
356 | post: {
|
357 | // same format as GET
|
358 | }
|
359 | },
|
360 | tests: function (deps) {
|
361 | it('responds', function (done) {
|
362 | deps.$http.get('/my/other/url')
|
363 | .then(function (response) {
|
364 | // response.status = 202
|
365 | // response.data = 42
|
366 | done();
|
367 | });
|
368 | http.flush();
|
369 | });
|
370 | }
|
371 | });
|
372 | ```
|
373 | All standard methods should be supported (`get`, `head`, `post`, `put`, `delete`, `jsonp` and `patch`).
|
374 |
|
375 | Each of the methods can return a function that returns an configuration object, see [mock http](#mock-http)
|
376 |
|
377 | **step** - shortcut for running the digest cycle and mock http flush
|
378 |
|
379 | ```js
|
380 | tests: function (deps) {
|
381 | it('runs the digest cycle', function (done) {
|
382 | $q.when(42).finally(done);
|
383 | deps.step();
|
384 | // same as deps.$rootScope.$digest();
|
385 | });
|
386 | }
|
387 | ```
|
388 |
|
389 | Also flushes the mock http backend
|
390 |
|
391 | ```js
|
392 | http: {}
|
393 | tests: function (deps) {
|
394 | it('returns expected result', function (done) {
|
395 | deps.$http.get(...)
|
396 | .then(...)
|
397 | .finally(done);
|
398 | deps.step();
|
399 | // same as deps.http.flush();
|
400 | });
|
401 | }
|
402 | ```
|
403 |
|
404 |
|
405 | ## Examples
|
406 |
|
407 | Some examples use Jasmine matchers, others use `la` assertion from
|
408 | [lazy-ass](https://github.com/bahmutov/lazy-ass) library and *done* callback argument
|
409 | from [Mocha](http://visionmedia.github.io/mocha/) testing framework.
|
410 |
|
411 | ### Test value provided by a module
|
412 |
|
413 | ```js
|
414 | // A.js
|
415 | angular.module('A', [])
|
416 | .value('foo', 'bar');
|
417 | // A-spec.js
|
418 | ngDescribe({
|
419 | name: 'test value',
|
420 | modules: 'A',
|
421 | inject: 'foo',
|
422 | tests: function (deps) {
|
423 | // deps object has every injected dependency as a property
|
424 | it('has correct value foo', function () {
|
425 | expect(deps.foo).toEqual('bar');
|
426 | });
|
427 | }
|
428 | });
|
429 | ```
|
430 |
|
431 | ### Test a filter
|
432 |
|
433 | We can easily test a built-in or custom filter function
|
434 |
|
435 | ```js
|
436 | ngDescribe({
|
437 | name: 'built-in filter',
|
438 | inject: '$filter',
|
439 | tests: function (deps) {
|
440 | it('can convert to lowercase', function () {
|
441 | var lowercase = deps.$filter('lowercase');
|
442 | la(lowercase('Foo') === 'foo');
|
443 | });
|
444 | }
|
445 | });
|
446 | ```
|
447 |
|
448 | ### Test a service
|
449 |
|
450 | We can inject a service to test using the same approach. You can even use multiple specs inside `tests` callback.
|
451 |
|
452 | ```js
|
453 | // B.js
|
454 | angular.module('B', ['A'])
|
455 | .service('addFoo', function (foo) {
|
456 | return function (str) {
|
457 | return str + foo;
|
458 | };
|
459 | });
|
460 | // B-spec.js
|
461 | ngDescribe({
|
462 | name: 'service tests',
|
463 | modules: 'B',
|
464 | inject: 'addFoo',
|
465 | tests: function (deps) {
|
466 | it('is a function', function () {
|
467 | expect(typeof deps.addFoo).toEqual('function');
|
468 | });
|
469 | it('appends value of foo to any string', function () {
|
470 | var result = deps.addFoo('x');
|
471 | expect(result).toEqual('xbar');
|
472 | });
|
473 | }
|
474 | });
|
475 | ```
|
476 |
|
477 | ### Test controller and scope
|
478 |
|
479 | We can easily create instances of controller functions and scope objects.
|
480 | In this example we also inject `$timeout` service to speed up delayed actions
|
481 | (see [Testing Angular async stuff](http://glebbahmutov.com/blog/testing-angular-async-stuff/)).
|
482 |
|
483 | ```js
|
484 | angular.module('S', [])
|
485 | .controller('sample', function ($timeout, $scope) {
|
486 | $scope.foo = 'foo';
|
487 | $scope.update = function () {
|
488 | $timeout(function () {
|
489 | $scope.foo = 'bar';
|
490 | }, 1000);
|
491 | };
|
492 | });
|
493 | ngDescribe({
|
494 | name: 'timeout in controller',
|
495 | modules: 'S',
|
496 | // inject $timeout so we can flush the timeout queue
|
497 | inject: ['$timeout'],
|
498 | controllers: 'sample',
|
499 | tests: function (deps) {
|
500 | // deps.sample = $scope object injected into sample controller
|
501 | it('has initial values', function () {
|
502 | la(deps.sample.foo === 'foo');
|
503 | });
|
504 | it('updates after timeout', function () {
|
505 | deps.sample.update();
|
506 | deps.$timeout.flush();
|
507 | la(deps.sample.foo === 'bar');
|
508 | });
|
509 | }
|
510 | });
|
511 | ```
|
512 |
|
513 | ### Test directive
|
514 |
|
515 | ```js
|
516 | angular.module('MyFoo', [])
|
517 | .directive('myFoo', function () {
|
518 | return {
|
519 | restrict: 'E',
|
520 | replace: true,
|
521 | template: '<span>{{ bar }}</span>'
|
522 | };
|
523 | });
|
524 | ngDescribe({
|
525 | name: 'MyFoo directive',
|
526 | modules: 'MyFoo',
|
527 | element: '<my-foo></my-foo>',
|
528 | tests: function (deps) {
|
529 | it('can update DOM using binding', function () {
|
530 | la(check.has(deps, 'element'), 'has compiled element');
|
531 | var scope = deps.element.scope();
|
532 | scope.bar = 'bar';
|
533 | scope.$apply();
|
534 | la(deps.element.html() === 'bar');
|
535 | });
|
536 | }
|
537 | });
|
538 | ```
|
539 |
|
540 | ### Test controllerAs syntax
|
541 |
|
542 | If you use `controllerAs` syntax without any components (see [Binding to ...][binding] post),
|
543 | then you can still test it quickly
|
544 |
|
545 | ```js
|
546 | angular.module('H', [])
|
547 | .controller('hController', function () {
|
548 | // notice we attach properties to the instance, not to the $scope
|
549 | this.foo = 'foo';
|
550 | });
|
551 | ngDescribe({
|
552 | module: 'H',
|
553 | element: '<div ng-controller="hController as ctrl">{{ ctrl.foo }}</div>',
|
554 | tests: function (deps) {
|
555 | it('created controller correctly', function () {
|
556 | var compiledHtml = deps.element.html();
|
557 | // 'foo'
|
558 | });
|
559 | it('changes value', function () {
|
560 | var ctrl = deps.element.controller();
|
561 | // { foo: 'foo' }
|
562 | ctrl.foo = 'bar';
|
563 | deps.element.scope().$apply();
|
564 | var compiledHtml = deps.element.html();
|
565 | // 'bar'
|
566 | });
|
567 | }
|
568 | });
|
569 | ```
|
570 |
|
571 | [binding]: http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html
|
572 |
|
573 | ### Test controller instance in custom directive
|
574 |
|
575 | If you add methods to the controller inside custom directive, use `controllerAs` syntax to
|
576 | expose the controller instance.
|
577 |
|
578 | ```js
|
579 | angular.module('C', [])
|
580 | .directive('cDirective', function () {
|
581 | return {
|
582 | controllerAs: 'ctrl', // puts controller instance onto scope as ctrl
|
583 | controller: function ($scope) {
|
584 | $scope.foo = 'foo';
|
585 | this.foo = function getFoo() {
|
586 | return $scope.foo;
|
587 | };
|
588 | }
|
589 | };
|
590 | });
|
591 | ngDescribe({
|
592 | name: 'controller for directive instance',
|
593 | modules: 'C',
|
594 | element: '<c-directive></c-directive>',
|
595 | tests: function (deps) {
|
596 | it('has controller', function () {
|
597 | var scope = deps.element.scope(); // grabs scope
|
598 | var controller = scope.ctrl; // grabs controller instance
|
599 | la(typeof controller.foo === 'function');
|
600 | la(controller.foo() === 'foo');
|
601 | scope.foo = 'bar';
|
602 | la(controller.foo() === 'bar');
|
603 | });
|
604 | }
|
605 | });
|
606 | ```
|
607 |
|
608 | ### Test 2 way binding
|
609 |
|
610 | If a directive implements isolate scope, we can configure parent scope separately.
|
611 |
|
612 | ```js
|
613 | angular.module('IsolateFoo', [])
|
614 | .directive('aFoo', function () {
|
615 | return {
|
616 | restrict: 'E',
|
617 | replace: true,
|
618 | scope: {
|
619 | bar: '='
|
620 | },
|
621 | template: '<span>{{ bar }}</span>'
|
622 | };
|
623 | });
|
624 | ```
|
625 |
|
626 | We can use `element` together with `parentScope` property to set initial values.
|
627 |
|
628 | ```js
|
629 | ngDescribe({
|
630 | modules: 'IsolateFoo',
|
631 | element: '<a-foo bar="x"></a-foo>',
|
632 | parentScope: {
|
633 | x: 'initial'
|
634 | },
|
635 | tests: function (deps) {
|
636 | it('has correct initial value', function () {
|
637 | var scope = deps.element.isolateScope();
|
638 | expect(scope.bar).toEqual('initial');
|
639 | });
|
640 | }
|
641 | });
|
642 | ```
|
643 |
|
644 | We can change parent's values to observe propagation into the directive
|
645 |
|
646 | ```js
|
647 | // same setup
|
648 | it('updates isolate scope', function () {
|
649 | deps.parentScope.x = 42;
|
650 | deps.$rootScope.$apply();
|
651 | var scope = deps.element.isolateScope();
|
652 | expect(scope.bar).toEqual(42);
|
653 | });
|
654 | ```
|
655 |
|
656 | ### Mock value provided by a module
|
657 |
|
658 | Often during testing we need to mock something provided by a module, even if it is
|
659 | passed via dependency injection. ng-describe makes it very simple. List all modules with values
|
660 | to be mocked in `mocks` object property.
|
661 |
|
662 | ```js
|
663 | // C.js
|
664 | angular.module('C', ['A'])
|
665 | .service('getFoo', function (foo) {
|
666 | // foo is provided by module A
|
667 | return function getFoo() {
|
668 | return foo;
|
669 | };
|
670 | });
|
671 | // C-spec.js
|
672 | ngDescribe({
|
673 | name: 'test C with mocking top level',
|
674 | modules: ['C'],
|
675 | inject: ['getFoo'],
|
676 | mocks: {
|
677 | // replace C.getFoo with mock function that returns 11
|
678 | C: {
|
679 | getFoo: function () {
|
680 | return 11;
|
681 | }
|
682 | }
|
683 | },
|
684 | verbose: false,
|
685 | tests: function (deps) {
|
686 | it('has mock injected value', function () {
|
687 | var result = deps.getFoo();
|
688 | la(result === 11, 'we got back mock value', result);
|
689 | });
|
690 | }
|
691 | });
|
692 | ```
|
693 |
|
694 | Remember when macking mocks, it is always `module name : provider name : mocked property name`
|
695 |
|
696 | ```js
|
697 | mocks: {
|
698 | 'module name': {
|
699 | 'mocked provider name': {
|
700 | 'mocked value name'
|
701 | }
|
702 | }
|
703 | }
|
704 | ```
|
705 |
|
706 | Note: the mocked values are injected using `$provider.constant` call to be able to override both
|
707 | values and constants
|
708 |
|
709 | ```js
|
710 | angular.module('A10', [])
|
711 | .constant('foo', 'bar');
|
712 | ngDescribe({
|
713 | modules: 'A10',
|
714 | mock: {
|
715 | A10: {
|
716 | foo: 42
|
717 | }
|
718 | },
|
719 | inject: 'foo',
|
720 | tests: function (deps) {
|
721 | it('has correct constant foo', function () {
|
722 | expect(deps.foo).toEqual(42);
|
723 | });
|
724 | }
|
725 | });
|
726 | ```
|
727 |
|
728 | ### Angular services inside mocks
|
729 |
|
730 | You can use other injected dependencies inside mocked functions, using
|
731 | injected values and free parameters.
|
732 |
|
733 | ```js
|
734 | ngDescribe({
|
735 | inject: ['getFoo', '$rootScope'],
|
736 | mocks: {
|
737 | C: {
|
738 | // use angular $q service in the mock function
|
739 | // argument "value" remains free
|
740 | getFoo: function ($q, value) {
|
741 | return $q.when(value);
|
742 | }
|
743 | }
|
744 | },
|
745 | tests: function (deps) {
|
746 | it('injected $q into mock', function (done) {
|
747 | deps.getFoo('foo').then(function (result) {
|
748 | expect(result).toEqual('foo');
|
749 | done();
|
750 | });
|
751 | deps.$rootScope.$apply(); // resolve promise
|
752 | });
|
753 | }
|
754 | });
|
755 | ```
|
756 |
|
757 | ### Mock $http.get
|
758 |
|
759 | Often we need some dummy response from `$http.get` method. We can use mock `httpBackend`
|
760 | or mock the `$http` object. For example to always return mock value when making any GET request,
|
761 | we can use
|
762 |
|
763 | ```js
|
764 | mocks: {
|
765 | ng: {
|
766 | $http: {
|
767 | get: function ($q, url) {
|
768 | // inspect url if needed
|
769 | return $q.when({
|
770 | data: {
|
771 | life: 42
|
772 | }
|
773 | });
|
774 | }
|
775 | }
|
776 | }
|
777 | }
|
778 | ```
|
779 |
|
780 | `$http` service returns a promise that resolves with a *response* object. The actual result to send
|
781 | is placed into the `data` property, as I show here.
|
782 |
|
783 | ### mock http
|
784 |
|
785 | You can use a shortcut to define mock HTTP responses via `$httpBackend` module. For example,
|
786 | you can define static responses
|
787 |
|
788 | ```js
|
789 | ngDescribe({
|
790 | http: {
|
791 | get: {
|
792 | '/some/url': 42,
|
793 | '/some/other/url': [500, 'something went wrong']
|
794 | },
|
795 | post: {
|
796 | // you can use custom functions too
|
797 | '/some/post/url': function (method, url, data, headers) {
|
798 | return [200, 'ok'];
|
799 | }
|
800 | }
|
801 | }
|
802 | });
|
803 | ```
|
804 | All HTTP methods are supported (`get`, `post`, `delete`, `put`, etc.)
|
805 |
|
806 | You can also get a function that would return a config object
|
807 |
|
808 | ```js
|
809 | var mockGetApi = {
|
810 | '/some/url': 42
|
811 | };
|
812 | mockGetApi['/some/other/url'] = [500, 'not ok'];
|
813 | ngDescribe({
|
814 | http: {
|
815 | get: mockGetApi
|
816 | }
|
817 | });
|
818 | ```
|
819 |
|
820 | You can use `deps.http.flush()` to move the http responses along.
|
821 |
|
822 | You can return the entire http mock object from a function, or combine objects with functions.
|
823 |
|
824 | ```js
|
825 | function constructMockApi() {
|
826 | return {
|
827 | get: function () {
|
828 | return { '/my/url': 42 };
|
829 | },
|
830 | post: {
|
831 | '/my/other/url': [200, 'nice']
|
832 | }
|
833 | };
|
834 | }
|
835 | ngDescribe({
|
836 | http: constructMockApi,
|
837 | test: function (deps) {
|
838 | ...
|
839 | }
|
840 | });
|
841 | ```
|
842 |
|
843 | You can use exact query arguments too
|
844 |
|
845 | ```js
|
846 | http: {
|
847 | get: {
|
848 | '/foo/bar?search=value': 42,
|
849 | '/foo/bar?search=value&something=else': 'foo'
|
850 | }
|
851 | }
|
852 | // $http.get('/foo/bar?search=value') will resolve with value 42
|
853 | // $http.get('/foo/bar?search=value&something=else') will resolve with value 'foo'
|
854 | ```
|
855 |
|
856 | or you can build the query string automatically by passing `params` property in the request config
|
857 | objet
|
858 |
|
859 | ```js
|
860 | http: {
|
861 | get: {
|
862 | '/foo/bar?search=value&something=else': 'foo'
|
863 | }
|
864 | }
|
865 | // inside the unit test
|
866 | var config = {
|
867 | params: {
|
868 | search: 'value',
|
869 | something: 'else'
|
870 | }
|
871 | };
|
872 | $http.get('/foo/bar', config).then(function (response) {
|
873 | // response.data = 'foo'
|
874 | });
|
875 | ```
|
876 |
|
877 | **note** the `http` mocks are defined using `$httpBack.when(method, ...)` calls,
|
878 | which are looser than `$httpBackend.expect(method, ...)`,
|
879 | see [ngMock/$httpBackend](https://docs.angularjs.org/api/ngMock/service/$httpBackend).
|
880 |
|
881 | ### beforeEach and afterEach
|
882 |
|
883 | You can use multiple `beforeEach` and `afterEach` inside `tests` function.
|
884 |
|
885 | ```js
|
886 | ngDescribe({
|
887 | name: 'before and after example',
|
888 | modules: ['A'],
|
889 | inject: ['foo'],
|
890 | tests: function (deps) {
|
891 | var localFoo;
|
892 | beforeEach(function () {
|
893 | // dependencies are already injected
|
894 | la(deps.foo === 'bar');
|
895 | localFoo = deps.foo;
|
896 | });
|
897 | it('has correct value foo', function () {
|
898 | la(localFoo === 'bar');
|
899 | });
|
900 | afterEach(function () {
|
901 | la(localFoo === 'bar');
|
902 | // dependencies are still available
|
903 | la(deps.foo === 'bar');
|
904 | });
|
905 | }
|
906 | });
|
907 | ```
|
908 |
|
909 | This could be useful for setting up additional mocks, like `$httpBackend`.
|
910 |
|
911 | ```js
|
912 | angular.module('apiCaller', [])
|
913 | .service('getIt', function ($http) {
|
914 | return function () {
|
915 | return $http.get('/my/url');
|
916 | };
|
917 | });
|
918 | ngDescribe({
|
919 | name: 'http mock backend example',
|
920 | modules: ['apiCaller'],
|
921 | inject: ['getIt', '$httpBackend'],
|
922 | tests: function (deps) {
|
923 | beforeEach(function () {
|
924 | deps.$httpBackend.expectGET('/my/url').respond(200, 42);
|
925 | });
|
926 | it('returns result from server', function (done) {
|
927 | deps.getIt().then(function (response) {
|
928 | la(response && response.status === 200);
|
929 | la(response.data === 42);
|
930 | done();
|
931 | });
|
932 | deps.$httpBackend.flush();
|
933 | });
|
934 | afterEach(function () {
|
935 | deps.$httpBackend.verifyNoOutstandingRequest();
|
936 | deps.$httpBackend.verifyNoOutstandingExpectation();
|
937 | });
|
938 | }
|
939 | });
|
940 | ```
|
941 |
|
942 | **Note** if you use `beforeEach` block with `element`, the `beforeEach` runs *before* the element
|
943 | is created. This gives you a chance to setup mocks before running the element and possibly making calls.
|
944 | If you really want to control when an element is created use `exposeApi` option
|
945 | (see [Secondary options](#secondary-options)).
|
946 |
|
947 | ### Spy on injected methods
|
948 |
|
949 | One can quickly spy on injected services (or other methods) using [sinon.js](http://sinonjs.org/)
|
950 | similarly to [spying on the regular JavaScript methods](http://glebbahmutov.com/blog/spying-on-methods/).
|
951 |
|
952 | * Include a browser-compatible combined [sinon.js build](http://sinonjs.org/releases/sinon-1.12.1.js)
|
953 | into the list of loaded Karma files.
|
954 | * Setup spy in the `beforeEach` function. Since every injected service is a method on the `deps`
|
955 | object, the setup is a single command.
|
956 | * Restore the original method in `afterEach` function.
|
957 |
|
958 | ```js
|
959 | // source code
|
960 | angular.module('Tweets', [])
|
961 | .service('getTweets', function () {
|
962 | return function getTweets(username) {
|
963 | console.log('returning # of tweets for', username);
|
964 | return 42;
|
965 | };
|
966 | });
|
967 | ```
|
968 |
|
969 | ```js
|
970 | // spec
|
971 | ngDescribe({
|
972 | name: 'spying on Tweets getTweets service',
|
973 | modules: 'Tweets',
|
974 | inject: 'getTweets',
|
975 | tests: function (deps) {
|
976 | beforeEach(function () {
|
977 | sinon.spy(deps, 'getTweets');
|
978 | });
|
979 | afterEach(function () {
|
980 | deps.getTweets.restore();
|
981 | });
|
982 | it('calls getTweets service', function () {
|
983 | var n = deps.getTweets('foo');
|
984 | la(n === 42, 'resolved with correct value');
|
985 | la(deps.getTweets.called, 'getTweets was called (spied using sinon)');
|
986 | la(deps.getTweets.firstCall.calledWith('foo'));
|
987 | });
|
988 | }
|
989 | });
|
990 | ```
|
991 |
|
992 | ### Spy on mocked service
|
993 |
|
994 | If we mock an injected service, we can still spy on it, just like as if we were spying on the
|
995 | regular service. For example, let us take the same method as above and mock it.
|
996 |
|
997 | ```js
|
998 | angular.module('Tweets', [])
|
999 | .service('getTweets', function () {
|
1000 | return function getTweets(username) {
|
1001 | return 42;
|
1002 | };
|
1003 | });
|
1004 | ```
|
1005 |
|
1006 | The mock will return a different number.
|
1007 |
|
1008 | ```js
|
1009 | ngDescribe({
|
1010 | name: 'spying on mock methods',
|
1011 | inject: 'getTweets',
|
1012 | mocks: {
|
1013 | Tweets: {
|
1014 | getTweets: function (username) {
|
1015 | return 1000;
|
1016 | }
|
1017 | }
|
1018 | },
|
1019 | tests: function (deps) {
|
1020 | beforeEach(function () {
|
1021 | sinon.spy(deps, 'getTweets');
|
1022 | });
|
1023 | afterEach(function () {
|
1024 | deps.getTweets.restore();
|
1025 | });
|
1026 | it('calls mocked getTweets service', function () {
|
1027 | var n = deps.getTweets('bar');
|
1028 | la(n === 1000, 'resolved with correct value from the mock service');
|
1029 | la(deps.getTweets.called,
|
1030 | 'mock service getTweets was called (spied using sinon)');
|
1031 | la(deps.getTweets.firstCall.calledWith('bar'),
|
1032 | 'mock service getTweets was called with expected argument');
|
1033 | });
|
1034 | }
|
1035 | });
|
1036 | ```
|
1037 |
|
1038 | ### Configure module
|
1039 |
|
1040 | If you use a separate module with namesake provider to pass configuration into the modules
|
1041 | (see [Inject valid constants into Angular](http://glebbahmutov.com/blog/inject-valid-constants-into-angular/)),
|
1042 | you can easily configure these modules.
|
1043 |
|
1044 | ```js
|
1045 | angular.module('App', ['AppConfig'])
|
1046 | .service('foo', function (AppConfig) {
|
1047 | return function foo() {
|
1048 | return GConfig.bar;
|
1049 | };
|
1050 | });
|
1051 | // config module has provider with same name
|
1052 | angular.module('AppConfig', [])
|
1053 | .provider('AppConfig', function () {
|
1054 | var config = {};
|
1055 | return {
|
1056 | set: function (settings) {
|
1057 | config = settings;
|
1058 | },
|
1059 | $get: function () {
|
1060 | return config;
|
1061 | }
|
1062 | };
|
1063 | });
|
1064 | // spec file
|
1065 | ngDescribe({
|
1066 | name: 'config module example',
|
1067 | modules: 'App',
|
1068 | inject: 'foo',
|
1069 | configs: {
|
1070 | // every config module will be loaded automatically
|
1071 | AppConfig: {
|
1072 | bar: 'boo!'
|
1073 | }
|
1074 | },
|
1075 | tests: function (deps) {
|
1076 | it('foo has configured bar value', function () {
|
1077 | expect(deps.foo()).toEqual('boo!');
|
1078 | });
|
1079 | }
|
1080 | });
|
1081 | ```
|
1082 |
|
1083 | You can configure multiple modules at the same time. Note that during the configuration
|
1084 | Angular is yet to be loaded. Thus you cannot use Angular services inside the configuration blocks.
|
1085 |
|
1086 | ### Helpful failure messages
|
1087 |
|
1088 | ng-describe works inside [helpDescribe function](https://github.com/bahmutov/lazy-ass-helpful#lazy-ass-helpful-bdd),
|
1089 | producing meaningful error messages on failure (if you use [lazy assertions](https://github.com/bahmutov/lazy-ass)).
|
1090 |
|
1091 | ```js
|
1092 | helpDescribe('ngDescribe inside helpful', function () {
|
1093 | ngDescribe({
|
1094 | name: 'example',
|
1095 | tests: function () {
|
1096 | it('gives helpful error message', function () {
|
1097 | var foo = 2, bar = 3;
|
1098 | la(foo + bar === 4); // wrong on purpose
|
1099 | });
|
1100 | }
|
1101 | });
|
1102 | });
|
1103 | ```
|
1104 | when this test fails, it generates a meaningful message with all relevant information: the expression
|
1105 | that fails `foo + bar === 4` and runtime values of `foo` and `bar`.
|
1106 |
|
1107 | PhantomJS 1.9.7 (Mac OS X)
|
1108 | ட ngDescribe inside helpful
|
1109 | ட example
|
1110 | ட ✘ gives helpful error message FAILED
|
1111 | Error: condition [foo + bar === 4] foo: 2 bar: 3
|
1112 | at lazyAss (/ng-describe/node_modules/lazy-ass/index.js:57)
|
1113 | PhantomJS 1.9.7 (Mac OS X): Executed 37 of 38 (1 FAILED) (skipped 1) (0.053 secs / 0.002 secs)
|
1114 |
|
1115 |
|
1116 | ## License
|
1117 |
|
1118 | Author: Kensho © 2014
|
1119 |
|
1120 | * [@kensho](https://twitter.com/kensho)
|
1121 | * [kensho.com](http://kensho.com)
|
1122 |
|
1123 | Support: if you find any problems with this library,
|
1124 | [open issue](https://github.com/kensho/ng-describe/issues) on Github
|
1125 |
|
1126 |
|
1127 | The MIT License (MIT)
|
1128 |
|
1129 | Copyright (c) 2014 Kensho
|
1130 |
|
1131 | Permission is hereby granted, free of charge, to any person obtaining a copy of
|
1132 | this software and associated documentation files (the "Software"), to deal in
|
1133 | the Software without restriction, including without limitation the rights to
|
1134 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
1135 | the Software, and to permit persons to whom the Software is furnished to do so,
|
1136 | subject to the following conditions:
|
1137 |
|
1138 | The above copyright notice and this permission notice shall be included in all
|
1139 | copies or substantial portions of the Software.
|
1140 |
|
1141 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
1142 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
1143 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
1144 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
1145 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
1146 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
1147 |
|
1148 |
|
1149 |
|