UNPKG

ng-describe

Version:

Convenient BDD specs for Angular

710 lines (630 loc) 17.6 kB
# Examples Some examples use Jasmine matchers, others use `la` assertion from [lazy-ass](https://github.com/bahmutov/lazy-ass) library and *done* callback argument from [Mocha](http://visionmedia.github.io/mocha/) testing framework. ## Test value provided by a module ```js // A.js angular.module('A', []) .value('foo', 'bar'); // A-spec.js ngDescribe({ name: 'test value', modules: 'A', inject: 'foo', tests: function (deps) { // deps object has every injected dependency as a property it('has correct value foo', function () { expect(deps.foo).toEqual('bar'); }); } }); ``` ## Test a filter We can easily test a built-in or custom filter function ```js ngDescribe({ name: 'built-in filter', inject: '$filter', tests: function (deps) { it('can convert to lowercase', function () { var lowercase = deps.$filter('lowercase'); la(lowercase('Foo') === 'foo'); }); } }); ``` ## Test a service We can inject a service to test using the same approach. You can even use multiple specs inside `tests` callback. ```js // B.js angular.module('B', ['A']) .service('addFoo', function (foo) { return function (str) { return str + foo; }; }); // B-spec.js ngDescribe({ name: 'service tests', modules: 'B', inject: 'addFoo', tests: function (deps) { it('is a function', function () { expect(typeof deps.addFoo).toEqual('function'); }); it('appends value of foo to any string', function () { var result = deps.addFoo('x'); expect(result).toEqual('xbar'); }); } }); ``` ## Test controller and scope We can easily create instances of controller functions and scope objects. In this example we also inject `$timeout` service to speed up delayed actions (see [Testing Angular async stuff](http://glebbahmutov.com/blog/testing-angular-async-stuff/)). ```js angular.module('S', []) .controller('sample', function ($timeout, $scope) { $scope.foo = 'foo'; $scope.update = function () { $timeout(function () { $scope.foo = 'bar'; }, 1000); }; }); ngDescribe({ name: 'timeout in controller', modules: 'S', // inject $timeout so we can flush the timeout queue inject: ['$timeout'], controllers: 'sample', tests: function (deps) { // deps.sample = $scope object injected into sample controller it('has initial values', function () { la(deps.sample.foo === 'foo'); }); it('updates after timeout', function () { deps.sample.update(); deps.$timeout.flush(); la(deps.sample.foo === 'bar'); }); } }); ``` ## Test directive ```js angular.module('MyFoo', []) .directive('myFoo', function () { return { restrict: 'E', replace: true, template: '<span>{{ bar }}</span>' }; }); ngDescribe({ name: 'MyFoo directive', modules: 'MyFoo', element: '<my-foo></my-foo>', tests: function (deps) { it('can update DOM using binding', function () { la(check.has(deps, 'element'), 'has compiled element'); var scope = deps.element.scope(); scope.bar = 'bar'; scope.$apply(); la(deps.element.html() === 'bar'); }); } }); ``` ## Test controllerAs syntax If you use `controllerAs` syntax without any components (see [Binding to ...][binding] post), then you can still test it quickly ```js angular.module('H', []) .controller('hController', function () { // notice we attach properties to the instance, not to the $scope this.foo = 'foo'; }); ngDescribe({ module: 'H', element: '<div ng-controller="hController as ctrl">{{ ctrl.foo }}</div>', tests: function (deps) { it('created controller correctly', function () { var compiledHtml = deps.element.html(); // 'foo' }); it('changes value', function () { var ctrl = deps.element.controller(); // { foo: 'foo' } ctrl.foo = 'bar'; deps.element.scope().$apply(); var compiledHtml = deps.element.html(); // 'bar' }); } }); ``` [binding]: http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html ## Test controller instance in custom directive If you add methods to the controller inside custom directive, use `controllerAs` syntax to expose the controller instance. ```js angular.module('C', []) .directive('cDirective', function () { return { controllerAs: 'ctrl', // puts controller instance onto scope as ctrl controller: function ($scope) { $scope.foo = 'foo'; this.foo = function getFoo() { return $scope.foo; }; } }; }); ngDescribe({ name: 'controller for directive instance', modules: 'C', element: '<c-directive></c-directive>', tests: function (deps) { it('has controller', function () { var scope = deps.element.scope(); // grabs scope var controller = scope.ctrl; // grabs controller instance la(typeof controller.foo === 'function'); la(controller.foo() === 'foo'); scope.foo = 'bar'; la(controller.foo() === 'bar'); }); } }); ``` ## Test 2 way binding If a directive implements isolate scope, we can configure parent scope separately. ```js angular.module('IsolateFoo', []) .directive('aFoo', function () { return { restrict: 'E', replace: true, scope: { bar: '=' }, template: '<span>{{ bar }}</span>' }; }); ``` We can use `element` together with `parentScope` property to set initial values. ```js ngDescribe({ modules: 'IsolateFoo', element: '<a-foo bar="x"></a-foo>', parentScope: { x: 'initial' }, tests: function (deps) { it('has correct initial value', function () { var scope = deps.element.isolateScope(); expect(scope.bar).toEqual('initial'); }); } }); ``` We can change parent's values to observe propagation into the directive ```js // same setup it('updates isolate scope', function () { deps.parentScope.x = 42; deps.$rootScope.$apply(); var scope = deps.element.isolateScope(); expect(scope.bar).toEqual(42); }); ``` ## Mock value provided by a module Often during testing we need to mock something provided by a module, even if it is passed via dependency injection. {%= name %} makes it very simple. List all modules with values to be mocked in `mocks` object property. ```js // C.js angular.module('C', ['A']) .service('getFoo', function (foo) { // foo is provided by module A return function getFoo() { return foo; }; }); // C-spec.js ngDescribe({ name: 'test C with mocking top level', modules: ['C'], inject: ['getFoo'], mocks: { // replace C.getFoo with mock function that returns 11 C: { getFoo: function () { return 11; } } }, verbose: false, tests: function (deps) { it('has mock injected value', function () { var result = deps.getFoo(); la(result === 11, 'we got back mock value', result); }); } }); ``` Remember when macking mocks, it is always `module name : provider name : mocked property name` ```js mocks: { 'module name': { 'mocked provider name': { 'mocked value name' } } } ``` Note: the mocked values are injected using `$provider.constant` call to be able to override both values and constants ```js angular.module('A10', []) .constant('foo', 'bar'); ngDescribe({ modules: 'A10', mock: { A10: { foo: 42 } }, inject: 'foo', tests: function (deps) { it('has correct constant foo', function () { expect(deps.foo).toEqual(42); }); } }); ``` ## Angular services inside mocks You can use other injected dependencies inside mocked functions, using injected values and free parameters. ```js ngDescribe({ inject: ['getFoo', '$rootScope'], mocks: { C: { // use angular $q service in the mock function // argument "value" remains free getFoo: function ($q, value) { return $q.when(value); } } }, tests: function (deps) { it('injected $q into mock', function (done) { deps.getFoo('foo').then(function (result) { expect(result).toEqual('foo'); done(); }); deps.$rootScope.$apply(); // resolve promise }); } }); ``` ## Mock $http.get Often we need some dummy response from `$http.get` method. We can use mock `httpBackend` or mock the `$http` object. For example to always return mock value when making any GET request, we can use ```js mocks: { ng: { $http: { get: function ($q, url) { // inspect url if needed return $q.when({ data: { life: 42 } }); } } } } ``` `$http` service returns a promise that resolves with a *response* object. The actual result to send is placed into the `data` property, as I show here. ## mock http You can use a shortcut to define mock HTTP responses via `$httpBackend` module. For example, you can define static responses ```js ngDescribe({ http: { get: { '/some/url': 42, '/some/other/url': [500, 'something went wrong'] }, post: { // you can use custom functions too '/some/post/url': function (method, url, data, headers) { return [200, 'ok']; } } } }); ``` All HTTP methods are supported (`get`, `post`, `delete`, `put`, etc.) You can also get a function that would return a config object ```js var mockGetApi = { '/some/url': 42 }; mockGetApi['/some/other/url'] = [500, 'not ok']; ngDescribe({ http: { get: mockGetApi } }); ``` You can use `deps.http.flush()` to move the http responses along. You can return the entire http mock object from a function, or combine objects with functions. ```js function constructMockApi() { return { get: function () { return { '/my/url': 42 }; }, post: { '/my/other/url': [200, 'nice'] } }; } ngDescribe({ http: constructMockApi, test: function (deps) { ... } }); ``` You can use exact query arguments too ```js http: { get: { '/foo/bar?search=value': 42, '/foo/bar?search=value&something=else': 'foo' } } // $http.get('/foo/bar?search=value') will resolve with value 42 // $http.get('/foo/bar?search=value&something=else') will resolve with value 'foo' ``` or you can build the query string automatically by passing `params` property in the request config objet ```js http: { get: { '/foo/bar?search=value&something=else': 'foo' } } // inside the unit test var config = { params: { search: 'value', something: 'else' } }; $http.get('/foo/bar', config).then(function (response) { // response.data = 'foo' }); ``` **note** the `http` mocks are defined using `$httpBack.when(method, ...)` calls, which are looser than `$httpBackend.expect(method, ...)`, see [ngMock/$httpBackend](https://docs.angularjs.org/api/ngMock/service/$httpBackend). ## beforeEach and afterEach You can use multiple `beforeEach` and `afterEach` inside `tests` function. ```js ngDescribe({ name: 'before and after example', modules: ['A'], inject: ['foo'], tests: function (deps) { var localFoo; beforeEach(function () { // dependencies are already injected la(deps.foo === 'bar'); localFoo = deps.foo; }); it('has correct value foo', function () { la(localFoo === 'bar'); }); afterEach(function () { la(localFoo === 'bar'); // dependencies are still available la(deps.foo === 'bar'); }); } }); ``` This could be useful for setting up additional mocks, like `$httpBackend`. ```js angular.module('apiCaller', []) .service('getIt', function ($http) { return function () { return $http.get('/my/url'); }; }); ngDescribe({ name: 'http mock backend example', modules: ['apiCaller'], inject: ['getIt', '$httpBackend'], tests: function (deps) { beforeEach(function () { deps.$httpBackend.expectGET('/my/url').respond(200, 42); }); it('returns result from server', function (done) { deps.getIt().then(function (response) { la(response && response.status === 200); la(response.data === 42); done(); }); deps.$httpBackend.flush(); }); afterEach(function () { deps.$httpBackend.verifyNoOutstandingRequest(); deps.$httpBackend.verifyNoOutstandingExpectation(); }); } }); ``` **Note** if you use `beforeEach` block with `element`, the `beforeEach` runs *before* the element is created. This gives you a chance to setup mocks before running the element and possibly making calls. If you really want to control when an element is created use `exposeApi` option (see [Secondary options](#secondary-options)). ## Spy on injected methods One can quickly spy on injected services (or other methods) using [sinon.js](http://sinonjs.org/) similarly to [spying on the regular JavaScript methods](http://glebbahmutov.com/blog/spying-on-methods/). * Include a browser-compatible combined [sinon.js build](http://sinonjs.org/releases/sinon-1.12.1.js) into the list of loaded Karma files. * Setup spy in the `beforeEach` function. Since every injected service is a method on the `deps` object, the setup is a single command. * Restore the original method in `afterEach` function. ```js // source code angular.module('Tweets', []) .service('getTweets', function () { return function getTweets(username) { console.log('returning # of tweets for', username); return 42; }; }); ``` ```js // spec ngDescribe({ name: 'spying on Tweets getTweets service', modules: 'Tweets', inject: 'getTweets', tests: function (deps) { beforeEach(function () { sinon.spy(deps, 'getTweets'); }); afterEach(function () { deps.getTweets.restore(); }); it('calls getTweets service', function () { var n = deps.getTweets('foo'); la(n === 42, 'resolved with correct value'); la(deps.getTweets.called, 'getTweets was called (spied using sinon)'); la(deps.getTweets.firstCall.calledWith('foo')); }); } }); ``` ## Spy on mocked service If we mock an injected service, we can still spy on it, just like as if we were spying on the regular service. For example, let us take the same method as above and mock it. ```js angular.module('Tweets', []) .service('getTweets', function () { return function getTweets(username) { return 42; }; }); ``` The mock will return a different number. ```js ngDescribe({ name: 'spying on mock methods', inject: 'getTweets', mocks: { Tweets: { getTweets: function (username) { return 1000; } } }, tests: function (deps) { beforeEach(function () { sinon.spy(deps, 'getTweets'); }); afterEach(function () { deps.getTweets.restore(); }); it('calls mocked getTweets service', function () { var n = deps.getTweets('bar'); la(n === 1000, 'resolved with correct value from the mock service'); la(deps.getTweets.called, 'mock service getTweets was called (spied using sinon)'); la(deps.getTweets.firstCall.calledWith('bar'), 'mock service getTweets was called with expected argument'); }); } }); ``` ## Configure module If you use a separate module with namesake provider to pass configuration into the modules (see [Inject valid constants into Angular](http://glebbahmutov.com/blog/inject-valid-constants-into-angular/)), you can easily configure these modules. ```js angular.module('App', ['AppConfig']) .service('foo', function (AppConfig) { return function foo() { return GConfig.bar; }; }); // config module has provider with same name angular.module('AppConfig', []) .provider('AppConfig', function () { var config = {}; return { set: function (settings) { config = settings; }, $get: function () { return config; } }; }); // spec file ngDescribe({ name: 'config module example', modules: 'App', inject: 'foo', configs: { // every config module will be loaded automatically AppConfig: { bar: 'boo!' } }, tests: function (deps) { it('foo has configured bar value', function () { expect(deps.foo()).toEqual('boo!'); }); } }); ``` You can configure multiple modules at the same time. Note that during the configuration Angular is yet to be loaded. Thus you cannot use Angular services inside the configuration blocks. ## Helpful failure messages {%= name %} works inside [helpDescribe function](https://github.com/bahmutov/lazy-ass-helpful#lazy-ass-helpful-bdd), producing meaningful error messages on failure (if you use [lazy assertions](https://github.com/bahmutov/lazy-ass)). ```js helpDescribe('ngDescribe inside helpful', function () { ngDescribe({ name: 'example', tests: function () { it('gives helpful error message', function () { var foo = 2, bar = 3; la(foo + bar === 4); // wrong on purpose }); } }); }); ``` when this test fails, it generates meaningful message with all relevant information: the expression that fails `foo + bar === 4` and runtime values of `foo` and `bar`. PhantomJS 1.9.7 (Mac OS X) ட ngDescribe inside helpful ட example ட ✘ gives helpful error message FAILED Error: condition [foo + bar === 4] foo: 2 bar: 3 at lazyAss (/ng-describe/node_modules/lazy-ass/index.js:57) PhantomJS 1.9.7 (Mac OS X): Executed 37 of 38 (1 FAILED) (skipped 1) (0.053 secs / 0.002 secs)