I use jasmine to write a unit test for a controller, where I have to check whether a function worked on $scope.$watch. I use 'controller as' syntax, but I've injected $scope to create a watcher. However, my test throws a vague exception on $scope.$apply(). Here's my controller:
OrderController.$inject = ['Order', 'cart',
'$mdDialog', '$state', '$rootScope', '$scope'];
function OrderController(Order, cart, $mdDialog, $state, $rootScope, $scope) {
var vm = this;
vm.order = new Order({
name: '',
phone: '',
address: ''
});
vm.order.items = [];
vm.promoCode = '';
vm.order.promo_code = false;
$scope.$watch('vm.promoCode', function () {
if (vm.promoCode && vm.promoCode == 'm7e17')) {
vm.order.promo_code = true;
}
}); }
And here's my test:
describe('Order Controller', function () {
var OrderController, CartService, $scope;
var item = {};
beforeEach(angular.mock.module('app'));
beforeEach(angular.mock.module('app.services'));
beforeEach(inject(function ($controller, _$rootScope_, _cart_, $state, $mdDialog) {
$scope = _$rootScope_.$new();
CartService = _cart_;
CartService.add(item);
OrderController = $controller('OrderController',
{
$scope: $scope,
$rootScope: _$rootScope_,
cart: CartService,
$state: $state,
$mdDialog: $mdDialog
}
);
}));
it('Should save a boolean whether a promo code has been entered', function () {
expect(OrderController.order.promo_code).toBe(false);
OrderController.promoCode = 'm7e17';
$scope.$apply();
expect(OrderController.order.promo_code).toBe(true);
}); });
However, when I call $scope.$apply inside my spec, it throws an error: 'Possibly unhandled rejection: ru thrown'. Could not find anything about it on the internet so, what I am I doing wrong here? I have other tests using the scope too, but they don't fail (though not calling $scope.$apply inside them), how can this be fixed?
I had recently the same issue.(Only with ' Possibly unhandled rejection: en thrown')
This issue is caused by the design of angular-translate, i assume you use this in your app.
Have a look at the following link:
How do unit test with angular-translate
There are some approaches to solve this issue.
I used the one which extracts the config of the translationProvider in an own file and in karma i exclude this one.
angular.module('qPro5App').config(loaderConfig);
loaderConfig.$inject = ['$translateProvider'];
function loaderConfig($translateProvider) {
// translation i18n support
$translateProvider.useStaticFilesLoader({
files: [{
prefix: 'assets/i18n/errors-',
suffix: '.json'
},
{
prefix: 'assets/i18n/ui-',
suffix: '.json'
}]
});
$translateProvider
.preferredLanguage('en')
.fallbackLanguage('en')
.useSanitizeValueStrategy('sanitize');
}
and in karma:
exclude: ['app/config/app-i18n-loader.js'],
I had the same problem in a slightly different context (handling a promise) and added
.catch(angular.noop)
which fixed the problem.
Related
I'm new to the unit testing in the client side. My application uses the express.js, angularjs-ui-router and node.js. Currently i start writing the unit test cases for the application. I'm using Karma, Mocha, Chai, Sinon for unit testing.
My router config look like below:
$stateProvider
.state('drive', {
url: '/drive',
templateUrl: 'drive.jade',
controller: 'driveCtrl',
});
Controller:
angular.module('mApp').controller('driveCtrl', ['$scope', 'driveService',
function($scope, driveService) {
var driveInfo = driveService.get({}, function() {});
driveInfo.$promise.then(function(rs) {
var drivers = [];
//Logical operation
$scope.drivers = drivers;
});
}]);
Factory Resource:
mApp.factory('driveService', ['$resource', function($resource) {
return $resource('/drive/id/:id/', {id:'#id'});
}]);
The driveService is a factory which insides uses a angular.js $resources. I tried variety of options but nothings seems to be working (using the $httpbackend, $q). Can you help me out to write the way to test the controller by mocking the driveService.
Here's my unit test code:
var expect = require('chai').expect;
var sinon = require('sinon');
describe('Drive Service initialisation', function() {
var scope, controller, state, $q, mockDriveService, driveServiceResponse = [{name:'james', type: 'heavy'}], queryDeferred;
beforeEach(angular.mock.module('mApp'));
angular.mock.module(function($provide){
$provide.value('driveService', mockDriveService);
});
describe(' drive service called', function() {
beforeEach(inject(function($controller, $rootScope, $state, _$q_, driveService) {
$q = _$q_;
scope = $rootScope.$new();
mockDriveService = {
get:function(){
queryDeferred = $q.defer();
queryDeferred.resolve(driveServiceResponse);
return {$promise: queryDeferred.promise};
}
};
controller = $controller('driveCtrl', { $scope: scope, driveService:driveService});
sinon.stub(mockDriveService, 'get');
scope.$apply();
}));
it('expect filter to be empty', function () {
expect(scope.drivers).to.not.be.empty;
});
});
});
The error what i'm getting is:
Error: Unexpected request: GET /driveService/id
No more request expected
at $httpBackend (node_modules/angular-mocks/angular-mocks.js:1210:9)
at sendReq (public/javascripts/lib/angular/angular.js:10333:9)
at serverRequest (public/javascripts/lib/angular/angular.js:10045:16)
at processQueue (public/javascripts/lib/angular/angular.js:14567:28)
at public/javascripts/lib/angular/angular.js:14583:27
at Scope.$eval (public/javascripts/lib/angular/angular.js:15846:28)
at Scope.$digest (public/javascripts/lib/angular/angular.js:15657:31)
at Scope.$apply (public/javascripts/lib/angular/angular.js:15951:24)
at Context.<anonymous> (C:/Users/por/AppData/Local/Temp/b0475694b46e0d60262621ad126ce46c.browserify:63:9)
at Object.invoke (public/javascripts/lib/angular/angular.js:4450:17)
Error: Declaration Location
at window.inject.angular.mock.inject (node_modules/angular-mocks/angular-mocks.js:2375:25)
You're defining a mocked service but still providing unmocked driveService for controller (this has been already done by default).
The fact that driveService.get is stubbed after the controller was instantiated and the method was called, doesn't help the case.
It should be something like
mockDriveService = {
get: sinon.stub().returns({$promise: $q.resolve(driveServiceResponse)})
};
controller = $controller('driveCtrl', { $scope: scope, driveService:mockDriveService});
scope.$apply();
The app should have test-friendly design to be tested efficiently. Considering that router is loaded in top-level module and its configuration is defined there too, app units that are supposed to be tested should be defined in child modules, so they could be tested in isolation.
The app may be refactored as
angular.module('mApp', ['ui.router', 'mApp.drive']);
...
angular.module('mApp.drive', [])
.controller('driveCtrl', ...)
.factory('driveService', ...);
And its units may be tested like
beforeEach(angular.mock.module('mApp.drive'));
...
I am running my tests with karma and phantom, Also I'm using mocha and sinon and tests are getting failed with below error:
EditResourceCategoryDialogTest EditResourceCategoryDialogController "before each" hook: workFn
Error: [$injector:modulerr] http://errors.angularjs.org/1.4.9/$injector/modulerr?p0=resourceofferingsApp&p1=Error%3A%20%5B%24injector%3Amodulerr%5D%20
Sample code:
define(function (require) {
"use strict";
var assert = require('chai').assert;
var sinon = require('sinon');
var angular = require('angular');
var angularMocks = require('angular.mocks');
require('resourceofferings/app');
require('dialog path');
describe('EditResourceCategoryDialogTest', function () {
beforeEach(module('resourceofferingsApp'));
describe('EditResourceCategoryDialogController', function () {
var $scope, ctrl;
//you need to inject dependencies first
beforeEach(inject(function ($rootScope, $injector) {
$scope = $rootScope.$new();
}));
it('initialization test (create mode)', inject(function ($controller) {
ctrl = $controller("EditResourceCategoryDialogController", {
$scope: $scope,
$uibModalInstance: null,
options: {
isEditMode: false
}
});
assert.equal($scope.isEditMode, false);
}));
});
});
});
Its exactly getting failed here:
beforeEach(inject(function ($rootScope, $injector) {
$scope = $rootScope.$new();
}));
Please help me to fix this issue..
Thanks in advance.
Try this ...
describe('controllers', function(){
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new(); // this is what you missed out
controller = $controller('EditResourceCategoryDialogController', {
$scope: scope,
$uibModalInstance: null,
options: {
isEditMode: false
}
});
}));
});
Update: According to Angular ...
A common reason why the module fails to load is that you've forgotten
to include the file with the defined module or that the file couldn't
be loaded.
Are you sure all needed files are loaded?
I'm trying to write a test for an Angular controller that mostly creates a datatable of values from the server. I've tried mocking DTOptionsBuilder and DTColumnBuilder but this doesn't seem to work. I get the error:
'undefined' is not an object (evaluating 'DTOptionsBuilder.fromFnPromise(function(){
return MarketsFactory.getAll();
})
.withDataProp')
Here is the Controller code:
.controller('MarketsCtrl', function($scope, $compile, $state, MarketsFactory,
DTOptionsBuilder, DTColumnBuilder) {
$scope.edit = function(data){
$state.go('admin.market', {id:data});
};
//DATATABLES CONFIGURATIONS
$scope.dtInstance = {};
$scope.dtOptions = DTOptionsBuilder.fromFnPromise(function(){
return MarketsFactory.getAll();
})
.withDataProp('data.data')
.withOption('createdRow', function(row, data, dataIndex) {
$compile(angular.element(row).contents())($scope);
})
.withTableTools('http://cdn.datatables.net/tabletools/2.2.2/swf/copy_csv_xls_pdf.swf')
.withTableToolsButtons([
'copy',
'print', {
'sExtends': 'collection',
'sButtonText': 'Save',
'aButtons': ['csv', 'xls', 'pdf']
}
])
.withBootstrap()
.withBootstrapOptions({
TableTools: {
classes: {
container: 'btn-group right',
buttons: {
normal: 'btn btn-outline btn-default btn-sm'
}
}
}
});
$scope.dtColumns = [
DTColumnBuilder.newColumn('shortName').withTitle('Short Name').withClass('dt-left'),
DTColumnBuilder.newColumn('name').withTitle('Name').withClass('dt-left'),
DTColumnBuilder.newColumn('timezone').withTitle('Time Zone').withClass('dt-left'),
DTColumnBuilder.newColumn('id').renderWith(function(data, type, full) {
return '<a ng-click="edit(\'' + data + '\')">Edit</a>';
})];
})
And the test file:
describe('Controller: MarketsCtrl', function () {
var scope, $state, DTOptionsBuilder, DTColumnBuilder;
beforeEach(function(){
var mockState = {};
var mockDTOptionsBuilder = {};
var mockDTColumnBuilder = {};
module('app', function($provide) {
$provide.value('$state', mockState);
$provide.value('DTOptionsBuilder', mockDTOptionsBuilder);
$provide.value('DTColumnBuilder', mockDTColumnBuilder);
});
inject(function() {
mockState.go = function(target) {
return target;
};
mockDTOptionsBuilder.fromFnPromise = jasmine.createSpy('DTOptionsBuilder.fromFnPromise');
mockDTOptionsBuilder.withDataProp = jasmine.createSpy('DTOptionsBuilder.withDataProp');
mockDTColumnBuilder.newColumn = jasmine.createSpy('DTColumnBuilder.newColumn');
});
});
beforeEach(inject(function ($controller, $rootScope, _$state_, _DTColumnBuilder_, _DTOptionsBuilder_) {
scope = $rootScope.$new();
$state = _$state_;
DTOptionsBuilder = _DTOptionsBuilder_;
DTColumnBuilder = _DTColumnBuilder_;
$controller('MarketsCtrl', {
$scope: scope,
$state: $state,
DTOptionsBuilder: DTOptionsBuilder,
DTColumnBuilder: DTColumnBuilder
});
scope.$digest();
}));
it('should provide an edit function', function () {
expect(typeof scope.edit).toBe('function');
});
});
I thought that creating a mock and putting a Spy on it would prevent it from calling the chained functions, but I guess not.
I'm quite new to testing, especially with Angular, so any help in general would be greatly appreciated!
I'm approaching this from a sinonjs/mocha background
I'm having some trouble decrypting your beforeEach block - but it can be simplified to some extent:
var $scope, $state, DTColumnBuilder, DTOptionsBuilder, createController;
beforeEach(function () {
DTColumnBuilder = {};
DTOptionsBuilder = {};
$state = {};
module('app', function ($provide) {
$provide.value('$state', $state);
$provide.value('DTColumnBuilder', DTColumnBuilder);
$provide.value('DTOptionsBuilder', DTOptionsBuilder);
});
inject(function ($controller, $injector) {
$scope = $injector.get('$rootScope').$new();
$state = $injector.get('$state');
DTColumnBuilder = $injector.get('DTColumnBuilder');
DTOptionsBuilder = $injector.get('DTOptionsBuilder');
createController = function () {
return $controller('MarketsCtrl', {
$scope: scope,
$state: $state,
DTOptionsBuilder: DTOptionsBuilder,
DTColumnBuilder: DTColumnBuilder
});
}
});
// Stub out the methods of interest.
DTOptionsBuilder.fromFnPromise = angular.noop;
$state.go = function () { console.log('I tried to go but.... I cant!!');
DTColumnBuilder.bar = function () { return 'bar'; };
});
The nature of a spy is that of letting the original implementation do its thing, but recording all of the calls to said function and an assortment of associated data.
A stub on the other hand is a spy with an extended API where you can completely modify the workings of said function. Return value, expected parameters, etc etc.
Assuming we used the aforementioned beforeEach block, DTOptionsBuilder.fromFnPromise would be a noop at this point. As such it would be safe to spy on it and expect the method to have been called.
it('should have been called', function () {
var spy = spyOn(DTOPtionsBuilder, 'fromFnPromise');
createController();
expect(spy).toHaveBeenCalled();
});
If you wanted to manipulate the return value of said function, I would grab sinonjs and make it a stub.
it('became "foo"', function () {
DTOptionsBuilder.fromFnPromise = sinon.stub().returns('foo');
createController();
expect($scope.dtOptions).toEqual('foo');
});
Now, since you are working with promises it's a wee bit more complicated but the basics of stubbing out a promise based function would be to:
Inject $q into your spec file.
Tell the stub to return $q.when(/** value **/) in the case of a resolved promise.
Tell the stub to return $q.reject(/** err **/) in the case of a rejected promise.
Run $timeout.flush() to flush all deferred tasks.
Trigger the done callback to notify Jasmine that you are done waiting for async tasks (may not be needed). This depends on the test framework/runner.
It could look something like so:
it('resolves with "foo"', function (done) {
DTOptionsBuilder.fromFnPromise = sinon.stub().returns($q.when('foo'));
expect($scope.options).to.eventually.become('foo').and.notify(done); // this is taken from the chai-as-promised library, I'm not sure what the Jasmine equivalent would be (if there is one).
createController();
$timeout.flush();
});
Now, a lot of this is just guesswork at this point. It's quite hard to set up a fully working test suite without having the source code running right beside me for cross reference, but I hope this will at least give you some ideas of how to get going with your spies and stubs.
I have been doing angularJS for a while now (without tests) but I want to do it properly! I have a controller defined like so
(function () {
'use strict';
angular.module('app')
.controller('CarehomeListCtrl', ['$scope', 'carehomesDataService', carehomeListCtrl]);
function carehomeListCtrl($scope, carehomesDataService) {
var vm = this;
vm.carehomeCollection = [];
vm.activate = activate;
function activate() {
vm.carehomeCollection = carehomesDataService.getAllCarehomes();
}
activate();
}
})();
and then my spec
describe("Carehomes tests", function () {
var $scopeConstructor, $controllerConstructor;
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope) {
$controllerConstructor = $controller;
$scopeConstructor = $rootScope;
}));
describe("CarehomeListCtrl", function () {
var ctrl, dataService, scope;
function createController() {
return $controllerConstructor('CarehomeListCtrl', {
$scope: scope,
carehomesDataService: dataService
});
}
beforeEach(inject(function ($injector) {
scope = $scopeConstructor.$new();
dataService =$injector.get('carehomesDataService') ;
}));
it("should have a carehomesCollection array", function () {
ctrl = createController();
expect(ctrl.carehomesCollection).not.toBeNull();
});
it("should have 3 items in carehomesCollection array when load is called", function () {
ctrl = createController();
expect(ctrl.carehomeCollection.length).toBe(3);
});
});
});
The problem here is that the call to instantiate my controller fails with error whenever I call it with any arguments whether an empty object {} or just $scope : scope} so I know the problem is not carehomesDataService.
Result StackTrace: Error: [ng:areq] Argument 'CarehomeListCtrl' is not
a function, got undefined
http://errors.angularjs.org/1.2.26/ng/areq?p0=CarehomeListCtrl&p1=not%20a%20function%2C%20got%20undefined
However, if I instantiate that controller like this $controllerConstructor('CarehomeListCtrl'); without arguments, it gets instantiated. I'm stumped!
carehomesDataService is a custom service I have written but it's own tests pass and it is correctly injected into the controller in the application.
Any help would be massively appreciated.
Note: I do not quite agree with defining properties on the controller as the view model instead of on $scope but I am following Jesse Liberty's pluralsight course and that's how he does it....plus injecting scope isn't quite working right now which is annoying. Thanks in advance.
I'm getting this error while I'm running unit test using Karma-Jasmine
ReferenceError: myModule is not defined
My sample test case is as follows..
describe("Unit Testing", function() {
beforeEach(angular.mock.module('myModule.common'));
var scope, ngTableParams, filter ,testTableParam;
it('should have a commonController controller', function () {
expect(myModule .common.controller('commonController ', function (commonController ) {
$scope:scope;
ngTableParams:ngTableParams;
$filter: filter;
tableParams: testTableParam
}
)).toBeDefined();
});});
I have injected the module name as myModule.common.
Can you please suggest a solution?
Try following code snippet it might help
describe('testing myModule.common', function() {
var $rootScope, $scope, $filter, $controller, ngTableParams, testTableParam;
beforeEach(module('myModule.common'));
beforeEach(function() {
inject(function($injector) {
$rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
$filter = $injector.get('$filter');
testTableParam = $injector.get('testTableParam');
ngTableParams = $injector.get('ngTableParams');
$controller = $injector.get('$controller')('commonController ', {
$scope: $scope
});
});
});
it('testing commonController ', function() {
expect('commonController ').toBeDefined();
});
});
It will solve your problem