UNPKG

21.6 kBMarkdownView Raw
1# Examples
2
3Some examples use Jasmine matchers, others use `la` assertion from
4[lazy-ass](https://github.com/bahmutov/lazy-ass) library and *done* callback argument
5from [Mocha](http://visionmedia.github.io/mocha/) testing framework.
6
7Also, note that the dependencies object is filled **only** inside the unit test callbacks `it` and
8setup helpers `beforeEach` and `afterEach`
9
10```js
11ngDescribe({
12 inject: 'foo',
13 tests: function (deps) {
14 // deps is an empty object here
15 beforeEach(function () {
16 // deps object has 'foo'
17 });
18 // deps is an empty object here
19 it(function () {
20 // deps object has 'foo'
21 });
22 // deps is an empty object here
23 afterEach(function () {
24 // deps object has 'foo'
25 });
26 }
27});
28```
29
30## Test value provided by a module
31
32```js
33// A.js
34angular.module('A', [])
35 .value('foo', 'bar');
36// A-spec.js
37ngDescribe({
38 name: 'test value',
39 modules: 'A',
40 inject: 'foo',
41 tests: function (deps) {
42 // deps object has every injected dependency as a property
43 it('has correct value foo', function () {
44 expect(deps.foo).toEqual('bar');
45 });
46 }
47});
48```
49
50## Test a filter
51
52We can easily test a built-in or custom filter function
53
54```js
55ngDescribe({
56 name: 'built-in filter',
57 inject: '$filter',
58 tests: function (deps) {
59 it('can convert to lowercase', function () {
60 var lowercase = deps.$filter('lowercase');
61 la(lowercase('Foo') === 'foo');
62 });
63 }
64});
65```
66
67## Test a service
68
69We can inject a service to test using the same approach. You can even use multiple specs inside `tests` callback.
70
71```js
72// B.js
73angular.module('B', ['A'])
74 .service('addFoo', function (foo) {
75 return function (str) {
76 return str + foo;
77 };
78 });
79// B-spec.js
80ngDescribe({
81 name: 'service tests',
82 modules: 'B',
83 inject: 'addFoo',
84 tests: function (deps) {
85 it('is a function', function () {
86 expect(typeof deps.addFoo).toEqual('function');
87 });
88 it('appends value of foo to any string', function () {
89 var result = deps.addFoo('x');
90 expect(result).toEqual('xbar');
91 });
92 }
93});
94```
95
96## Test controller and scope
97
98We can easily create instances of controller functions and scope objects.
99In this example we also inject `$timeout` service to speed up delayed actions
100(see [Testing Angular async stuff](http://glebbahmutov.com/blog/testing-angular-async-stuff/)).
101
102```js
103angular.module('S', [])
104 .controller('sample', function ($timeout, $scope) {
105 $scope.foo = 'foo';
106 $scope.update = function () {
107 $timeout(function () {
108 $scope.foo = 'bar';
109 }, 1000);
110 };
111 });
112ngDescribe({
113 name: 'timeout in controller',
114 modules: 'S',
115 // inject $timeout so we can flush the timeout queue
116 inject: ['$timeout'],
117 controllers: 'sample',
118 tests: function (deps) {
119 // deps.sample = $scope object injected into sample controller
120 it('has initial values', function () {
121 la(deps.sample.foo === 'foo');
122 });
123 it('updates after timeout', function () {
124 deps.sample.update();
125 deps.$timeout.flush();
126 la(deps.sample.foo === 'bar');
127 });
128 }
129});
130```
131
132## Test directive
133
134```js
135angular.module('MyFoo', [])
136 .directive('myFoo', function () {
137 return {
138 restrict: 'E',
139 replace: true,
140 template: '<span>{{ bar }}</span>'
141 };
142 });
143ngDescribe({
144 name: 'MyFoo directive',
145 modules: 'MyFoo',
146 element: '<my-foo></my-foo>',
147 tests: function (deps) {
148 it('can update DOM using binding', function () {
149 la(check.has(deps, 'element'), 'has compiled element');
150 var scope = deps.element.scope();
151 scope.bar = 'bar';
152 scope.$apply();
153 la(deps.element.html() === 'bar');
154 });
155 }
156});
157```
158
159## Test controllerAs syntax
160
161If you use `controllerAs` syntax without any components (see [Binding to ...][binding] post or
162[Separate ...][separate]), then you can still test it quickly
163
164```js
165angular.module('H', [])
166 .controller('hController', function () {
167 // notice we attach properties to the instance, not to the $scope
168 this.foo = 'foo';
169 });
170 ngDescribe({
171 module: 'H',
172 element: '<div ng-controller="hController as ctrl">{{ ctrl.foo }}</div>',
173 tests: function (deps) {
174 it('created controller correctly', function () {
175 var compiledHtml = deps.element.html();
176 // 'foo'
177 });
178 it('changes value', function () {
179 var ctrl = deps.element.controller();
180 // { foo: 'foo' }
181 ctrl.foo = 'bar';
182 deps.element.scope().$apply();
183 var compiledHtml = deps.element.html();
184 // 'bar'
185 });
186 }
187 });
188```
189
190[binding]: http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html
191[separate]: http://glebbahmutov.com/blog/separate-model-from-view-in-angular/
192
193## Test controller instance in custom directive
194
195If you add methods to the controller inside custom directive, use `controllerAs` syntax to
196expose the controller instance.
197
198```js
199angular.module('C', [])
200 .directive('cDirective', function () {
201 return {
202 controllerAs: 'ctrl', // puts controller instance onto scope as ctrl
203 controller: function ($scope) {
204 $scope.foo = 'foo';
205 this.foo = function getFoo() {
206 return $scope.foo;
207 };
208 }
209 };
210 });
211ngDescribe({
212 name: 'controller for directive instance',
213 modules: 'C',
214 element: '<c-directive></c-directive>',
215 tests: function (deps) {
216 it('has controller', function () {
217 var scope = deps.element.scope(); // grabs scope
218 var controller = scope.ctrl; // grabs controller instance
219 la(typeof controller.foo === 'function');
220 la(controller.foo() === 'foo');
221 scope.foo = 'bar';
222 la(controller.foo() === 'bar');
223 });
224 }
225});
226```
227
228## Test 2 way binding
229
230If a directive implements isolate scope, we can configure parent scope separately.
231
232```js
233angular.module('IsolateFoo', [])
234 .directive('aFoo', function () {
235 return {
236 restrict: 'E',
237 replace: true,
238 scope: {
239 bar: '='
240 },
241 template: '<span>{{ bar }}</span>'
242 };
243 });
244```
245
246We can use `element` together with `parentScope` property to set initial values.
247
248```js
249ngDescribe({
250 modules: 'IsolateFoo',
251 element: '<a-foo bar="x"></a-foo>',
252 parentScope: {
253 x: 'initial'
254 },
255 tests: function (deps) {
256 it('has correct initial value', function () {
257 var scope = deps.element.isolateScope();
258 expect(scope.bar).toEqual('initial');
259 });
260 }
261});
262```
263
264We can change parent's values to observe propagation into the directive
265
266```js
267// same setup
268it('updates isolate scope', function () {
269 deps.parentScope.x = 42;
270 deps.$rootScope.$apply();
271 var scope = deps.element.isolateScope();
272 expect(scope.bar).toEqual(42);
273});
274```
275
276## beforeEach and afterEach
277
278You can use multiple `beforeEach` and `afterEach` inside `tests` function.
279
280```js
281ngDescribe({
282 name: 'before and after example',
283 modules: ['A'],
284 inject: ['foo'],
285 tests: function (deps) {
286 var localFoo;
287 beforeEach(function () {
288 // dependencies are already injected
289 la(deps.foo === 'bar');
290 localFoo = deps.foo;
291 });
292 it('has correct value foo', function () {
293 la(localFoo === 'bar');
294 });
295 afterEach(function () {
296 la(localFoo === 'bar');
297 // dependencies are still available
298 la(deps.foo === 'bar');
299 });
300 }
301});
302```
303
304This could be useful for setting up additional mocks, like `$httpBackend`.
305
306```js
307angular.module('apiCaller', [])
308 .service('getIt', function ($http) {
309 return function () {
310 return $http.get('/my/url');
311 };
312 });
313ngDescribe({
314 name: 'http mock backend example',
315 modules: ['apiCaller'],
316 inject: ['getIt', '$httpBackend'],
317 tests: function (deps) {
318 beforeEach(function () {
319 deps.$httpBackend.expectGET('/my/url').respond(200, 42);
320 });
321 it('returns result from server', function (done) {
322 deps.getIt().then(function (response) {
323 la(response && response.status === 200);
324 la(response.data === 42);
325 done();
326 });
327 deps.$httpBackend.flush();
328 });
329 afterEach(function () {
330 deps.$httpBackend.verifyNoOutstandingRequest();
331 deps.$httpBackend.verifyNoOutstandingExpectation();
332 });
333 }
334});
335```
336
337**Note** if you use `beforeEach` block with `element`, the `beforeEach` runs *before* the element
338is created. This gives you a chance to setup mocks before running the element and possibly making calls.
339If you really want to control when an element is created use `exposeApi` option
340(see [Secondary options](#secondary-options)).
341
342## Mocking
343
344### Mock value provided by a module
345
346Often during testing we need to mock something provided by a module, even if it is
347passed via dependency injection. {%= name %} makes it very simple. List all modules with values
348to be mocked in `mocks` object property.
349
350```js
351// C.js
352angular.module('C', ['A'])
353 .service('getFoo', function (foo) {
354 // foo is provided by module A
355 return function getFoo() {
356 return foo;
357 };
358 });
359// C-spec.js
360ngDescribe({
361 name: 'test C with mocking top level',
362 modules: ['C'],
363 inject: ['getFoo'],
364 mocks: {
365 // replace C.getFoo with mock function that returns 11
366 C: {
367 getFoo: function () {
368 return 11;
369 }
370 }
371 },
372 verbose: false,
373 tests: function (deps) {
374 it('has mock injected value', function () {
375 var result = deps.getFoo();
376 la(result === 11, 'we got back mock value', result);
377 });
378 }
379});
380```
381
382Remember when making mocks, it is always `module name : provider name : mocked property name`
383
384```js
385mocks: {
386 'module name': {
387 'mocked provider name': {
388 'mocked value name'
389 }
390 }
391}
392```
393
394Note: the mocked values are injected using `$provider.constant` call to be able to override both
395values and constants
396
397```js
398angular.module('A10', [])
399 .constant('foo', 'bar');
400ngDescribe({
401 modules: 'A10',
402 mock: {
403 A10: {
404 foo: 42
405 }
406 },
407 inject: 'foo',
408 tests: function (deps) {
409 it('has correct constant foo', function () {
410 expect(deps.foo).toEqual(42);
411 });
412 }
413});
414```
415
416You can even mock part of the module itself and use mock value in other parts via injection
417
418```js
419angular.module('LargeModule', [])
420 .constant('foo', 'foo')
421 .service('getFoo', function (foo) {
422 return function getFoo() {
423 return foo;
424 };
425 });
426ngDescribe({
427 name: 'mocking part of the module itself',
428 modules: 'LargeModule',
429 inject: 'getFoo',
430 mock: {
431 LargeModule: {
432 foo: 'bar'
433 }
434 },
435 tests: function (deps) {
436 it('service injects mock value', function () {
437 la(deps.getFoo() === 'bar', 'returns mock value');
438 });
439 }
440});
441```
442
443### Angular services inside mocks
444
445You can use other injected dependencies inside mocked functions, using
446injected values and free parameters.
447
448```js
449ngDescribe({
450 inject: ['getFoo', '$rootScope'],
451 mocks: {
452 C: {
453 // use angular $q service in the mock function
454 // argument "value" remains free
455 getFoo: function ($q, value) {
456 return $q.when(value);
457 }
458 }
459 },
460 tests: function (deps) {
461 it('injected $q into mock', function (done) {
462 deps.getFoo('foo').then(function (result) {
463 expect(result).toEqual('foo');
464 done();
465 });
466 deps.$rootScope.$apply(); // resolve promise
467 });
468 }
469});
470```
471
472### Mock $http.get
473
474Often we need some dummy response from `$http.get` method. We can use mock `httpBackend`
475or mock the `$http` object. For example to always return mock value when making any GET request,
476we can use
477
478```js
479mocks: {
480 ng: {
481 $http: {
482 get: function ($q, url) {
483 // inspect url if needed
484 return $q.when({
485 data: {
486 life: 42
487 }
488 });
489 }
490 }
491 }
492}
493```
494
495`$http` service returns a promise that resolves with a *response* object. The actual result to send
496is placed into the `data` property, as I show here.
497
498### Mock http responses
499
500You can use a shortcut to define mock HTTP responses via `$httpBackend` module. For example,
501you can define static responses.
502
503```js
504ngDescribe({
505 http: {
506 get: {
507 '/some/url': 42,
508 '/some/other/url': [500, 'something went wrong']
509 },
510 post: {
511 // you can use custom functions too
512 '/some/post/url': function (method, url, data, headers) {
513 return [200, 'ok'];
514 }
515 }
516 }
517});
518```
519All HTTP methods are supported (`get`, `post`, `delete`, `put`, etc.).
520
521You can also get a function that would return a config object.
522
523```js
524var mockGetApi = {
525 '/some/url': 42
526};
527mockGetApi['/some/other/url'] = [500, 'not ok'];
528ngDescribe({
529 http: {
530 get: mockGetApi
531 }
532});
533```
534
535You can use `deps.http.flush()` to move the http responses along.
536
537You can return the entire http mock object from a function, or combine objects with functions.
538
539```js
540function constructMockApi() {
541 return {
542 get: function () {
543 return { '/my/url': 42 };
544 },
545 post: {
546 '/my/other/url': [200, 'nice']
547 }
548 };
549}
550ngDescribe({
551 http: constructMockApi,
552 test: function (deps) {
553 ...
554 }
555});
556```
557
558You can use exact query arguments too
559
560```js
561http: {
562 get: {
563 '/foo/bar?search=value': 42,
564 '/foo/bar?search=value&something=else': 'foo'
565 }
566}
567// $http.get('/foo/bar?search=value') will resolve with value 42
568// $http.get('/foo/bar?search=value&something=else') will resolve with value 'foo'
569```
570
571or you can build the query string automatically by passing `params` property in the request config
572objet
573
574```js
575http: {
576 get: {
577 '/foo/bar?search=value&something=else': 'foo'
578 }
579}
580// inside the unit test
581var config = {
582 params: {
583 search: 'value',
584 something: 'else'
585 }
586};
587$http.get('/foo/bar', config).then(function (response) {
588 // response.data = 'foo'
589});
590```
591
592**note** the `http` mocks are defined using `$httpBack.when(method, ...)` calls,
593which are looser than `$httpBackend.expect(method, ...)`,
594see [ngMock/$httpBackend](https://docs.angularjs.org/api/ngMock/service/$httpBackend).
595
596## Spying
597
598### Spy on injected methods
599
600One can quickly spy on injected services (or other methods) using [sinon.js](http://sinonjs.org/)
601similarly to [spying on the regular JavaScript methods](http://glebbahmutov.com/blog/spying-on-methods/).
602
603* Include a browser-compatible combined [sinon.js build](http://sinonjs.org/releases/sinon-1.12.1.js)
604into the list of loaded Karma files.
605* Setup spy in the `beforeEach` function. Since every injected service is a method on the `deps`
606object, the setup is a single command.
607* Restore the original method in `afterEach` function.
608
609```js
610// source code
611angular.module('Tweets', [])
612 .service('getTweets', function () {
613 return function getTweets(username) {
614 console.log('returning # of tweets for', username);
615 return 42;
616 };
617 });
618```
619
620```js
621// spec
622ngDescribe({
623 name: 'spying on Tweets getTweets service',
624 modules: 'Tweets',
625 inject: 'getTweets',
626 tests: function (deps) {
627 beforeEach(function () {
628 sinon.spy(deps, 'getTweets');
629 });
630 afterEach(function () {
631 deps.getTweets.restore();
632 });
633 it('calls getTweets service', function () {
634 var n = deps.getTweets('foo');
635 la(n === 42, 'resolved with correct value');
636 la(deps.getTweets.called, 'getTweets was called (spied using sinon)');
637 la(deps.getTweets.firstCall.calledWith('foo'));
638 });
639 }
640});
641```
642
643### Spy on injected function
644
645You can inject a function, but use a [Sinon spy](http://sinonjs.org/docs/#spies) instead
646of the injected function to get additional information. For example, to spy on the `$filter uppercase`,
647we can use the following code.
648
649```js
650ngDescribe({
651 name: 'spying on a filter',
652 inject: '$filter',
653 tests: function (deps) {
654 /*
655 to spy on a injected filter, need to grab the actual filter function
656 and then create a spy
657 */
658 // _uppercase = angular uppercase $filter
659 // uppercase = spy on the _uppercase
660 var _uppercase, uppercase;
661 beforeEach(function () {
662 _uppercase = deps.$filter('uppercase');
663 uppercase = sinon.spy(_uppercase);
664 });
665 it('converts string to uppercase', function () {
666 var result = uppercase('foo');
667 la(result === 'FOO', 'converted string to uppercase', result);
668 la(uppercase.calledOnce, 'uppercase was called once');
669 la(uppercase.calledWith('foo'));
670 });
671 }
672});
673```
674
675### Spy on 3rd party service injected some place else
676
677Let us say you need to verify that the `$interval` service injected in the module under test
678was called. It is a little verbose to verify from the unit test. We must mock the `$interval`
679with our function and then call the actual `$interval` from the module `ng` to provide the
680same functionality.
681
682Source code we are trying to unit test
683
684```js
685angular.module('IntervalExample', [])
686 .service('numbers', function ($interval, $rootScope) {
687 return function emitNumbers(delay, n) {
688 var k = 0;
689 $interval(function () {
690 $rootScope.$emit('number', k);
691 k += 1;
692 }, 100, n);
693 };
694 });
695```
696
697In the unit test we will mock `$interval` service for module `IntervalExample`
698
699```js
700// unit test start
701var intervalCalled;
702ngDescribe({
703 name: 'spying on $interval',
704 module: 'IntervalExample',
705 inject: ['numbers', '$rootScope'],
706 verbose: false,
707 only: false,
708 mocks: {
709 IntervalExample: {
710 $interval: function mockInterval(fn, delay, n) {
711 var injector = angular.injector(['ng']);
712 var $interval = injector.get('$interval');
713 intervalCalled = true;
714 return $interval(fn, delay, n);
715 }
716 }
717 },
718 tests: function (deps) {
719 // unit test goes here
720 }
721});
722```
723
724A unit test just calls the `numbers` function and then checks the variable `intervalCalled`
725
726```js
727it('emits 3 numbers', function (done) {
728 deps.$rootScope.$on('number', function (event, k) {
729 if (k === 2) {
730 done();
731 }
732 });
733 // emit 3 numbers with 100ms interval
734 deps.numbers(100, 3);
735 la(intervalCalled, 'the $interval was called somewhere');
736});
737```
738
739You can see the unit test in file [test/spying-on-interval-spec.js](test/spying-on-interval-spec.js).
740
741### Spy on mocked service
742
743If we mock an injected service, we can still spy on it, just like as if we were spying on the
744regular service. For example, let us take the same method as above and mock it.
745
746```js
747angular.module('Tweets', [])
748 .service('getTweets', function () {
749 return function getTweets(username) {
750 return 42;
751 };
752 });
753```
754
755The mock will return a different number.
756
757```js
758ngDescribe({
759 name: 'spying on mock methods',
760 inject: 'getTweets',
761 mocks: {
762 Tweets: {
763 getTweets: function (username) {
764 return 1000;
765 }
766 }
767 },
768 tests: function (deps) {
769 beforeEach(function () {
770 sinon.spy(deps, 'getTweets');
771 });
772 afterEach(function () {
773 deps.getTweets.restore();
774 });
775 it('calls mocked getTweets service', function () {
776 var n = deps.getTweets('bar');
777 la(n === 1000, 'resolved with correct value from the mock service');
778 la(deps.getTweets.called,
779 'mock service getTweets was called (spied using sinon)');
780 la(deps.getTweets.firstCall.calledWith('bar'),
781 'mock service getTweets was called with expected argument');
782 });
783 }
784});
785```
786
787## Configure module
788
789If you use a separate module with namesake provider to pass configuration into the modules
790(see [Inject valid constants into Angular](http://glebbahmutov.com/blog/inject-valid-constants-into-angular/)),
791you can easily configure these modules.
792
793```js
794angular.module('App', ['AppConfig'])
795 .service('foo', function (AppConfig) {
796 return function foo() {
797 return GConfig.bar;
798 };
799 });
800// config module has provider with same name
801angular.module('AppConfig', [])
802 .provider('AppConfig', function () {
803 var config = {};
804 return {
805 set: function (settings) {
806 config = settings;
807 },
808 $get: function () {
809 return config;
810 }
811 };
812 });
813// spec file
814ngDescribe({
815 name: 'config module example',
816 modules: 'App',
817 inject: 'foo',
818 configs: {
819 // every config module will be loaded automatically
820 AppConfig: {
821 bar: 'boo!'
822 }
823 },
824 tests: function (deps) {
825 it('foo has configured bar value', function () {
826 expect(deps.foo()).toEqual('boo!');
827 });
828 }
829});
830```
831
832You can configure multiple modules at the same time. Note that during the configuration
833Angular is yet to be loaded. Thus you cannot use Angular services inside the configuration blocks.
834
835## Helpful failure messages
836
837{%= name %} works inside [helpDescribe function](https://github.com/bahmutov/lazy-ass-helpful#lazy-ass-helpful-bdd),
838producing meaningful error messages on failure (if you use [lazy assertions](https://github.com/bahmutov/lazy-ass)).
839
840```js
841helpDescribe('ngDescribe inside helpful', function () {
842 ngDescribe({
843 name: 'example',
844 tests: function () {
845 it('gives helpful error message', function () {
846 var foo = 2, bar = 3;
847 la(foo + bar === 4); // wrong on purpose
848 });
849 }
850 });
851});
852```
853when this test fails, it generates meaningful message with all relevant information: the expression
854that fails `foo + bar === 4` and runtime values of `foo` and `bar`.
855
856 PhantomJS 1.9.7 (Mac OS X)
857 ட ngDescribe inside helpful
858 ட example
859 ட ✘ gives helpful error message FAILED
860 Error: condition [foo + bar === 4] foo: 2 bar: 3
861 at lazyAss (/ng-describe/node_modules/lazy-ass/index.js:57)
862 PhantomJS 1.9.7 (Mac OS X): Executed 37 of 38 (1 FAILED) (skipped 1) (0.053 secs / 0.002 secs)