Angular Service becomes undefined when spyOn it - javascript

I have an Angular 1.6.6 application which I'm testing with Karma and Jasmine.
Given this code from controller:
$scope.undo = function () {
return $scope.isUndoDisabled() || $scope.undoAction();
};
$scope.isUndoDisabled = function () {
return HistoryService.isUndoDisabled();
};
I have been testing it with the following spec:
it('undoAction should not be called with default settings', function () {
var $scope = {};
var controller = $controller('PaintController', { $scope: $scope });
spyOn($scope, 'undoAction');
//spyOn(HistoryService, 'isUndoDisabled');
$scope.undo();
expect($scope.undoAction).not.toHaveBeenCalled();
});
And is passing the test, but when I uncomment the spyOn of HistoryService, the call HistoryService.isUndoDisabled() from $scope.isUndoDisabled returns undefined and then the test fails because:
Expected spy undoAction not to have been called.
Any idea about what's going on???? It seems like the spyOn is affecting to the code??

spyOn(...) is a shortcut for spyOn(...).and.stub(), not spyOn(...).and.callThrough(). When being spied this way, HistoryService.isUndoDisabled() returns undefined.
The proper way to test the unit is to isolate it from others. Since it is the controller that is tested, the service should be mocked or stubbed:
spyOn(HistoryService, 'isUndoDisabled').and.returnValue(true);
And then in another test:
spyOn(HistoryService, 'isUndoDisabled').and.returnValue(false);

I think if you want to call isUndoDisabled() from HistoryService, the function $scope.isUndoDisabled should be
$scope.isUndoDisabled = function () {
HistoryService.isUndoDisabled();
};
There shouldn't be a return in the body

Related

Test a callback method's functionality in Jasmine

I have a service as following.
InvService(...){
this.getROItems = function(cb){
$http.get('url').success(cb);
}
}
One of the controllers which uses the above:
var roItems = [];
InvService.getROItems(function(res){
roItems = res.lts.items;
});
In Jasmine, I want to test that roItems are assigned the values from the response. How can I achieve this?
I'd recommend that you have separated tests for you service and for your controller. If you want to test that roItems was assigned, you need to test your controller. Then, you could mock your service since it is not relevant for the controller test and make it return whatever you want. You need something like this:
describe('my awesome test', function() {
it('my awesome test block',
inject(function(InvService, $controller) {
//This mocks your service with a fake implementation.
//Note that I mocked before the controller initialization.
spyOn(InvService, 'getROItems').and.callFake(function(cb){
var resultFake = {
lts: {
items: "whatever you want"
}
}
cb(resultFake);
});
//This initializes your controller and it will use the mocked
//implementation of your service
var myController = $controller("myControllerName");
//Here we make the assertio
expect(myController.roItems).toBe("whatever you want");
}
)
});

Unit Testing a Controller with specific syntax

Whenever, I am testing a controller and have something like this in it.
$scope.isSomething = function (Item) {
return ItemCollection.someItem(Item.attachedItem);
};
giving error on karma console:
TypeError: undefined is not an object (evaluating 'Item.attachedItem')
I am simply calling the function from the test file like this:
scope.isSomething();
I need to mock the Item.attachedItem or I am missing something here.. Please Explain in details as this is happening in multiple files.. thanks in advance
Also, for this type of code
.controller('itemCtrl', function (itemCollection) {
var vm = this;
this.itemCollection= itemCollection;
itemCollection.someItem().then(function (Item) {
vm.pageUrl = Item.pageUrl;
vm.Item= Item.someItems;
});
});
Also, this is also part of the code for more broad view here it gives Item.pageUrl is not a object error
Refer angular unit testing docs
The ItemCollection being a service, you could inject a mock while initialising a controller using
var ItemCollection, ItemCrtl;
beforeEach(inject(function($controller, $rootScope) {
$scope = $rootScope.$new();
ItemCollection = jasmine.createSpyObj('ItemCollection', ['someItem']);
ItemCrtl = $controller('ItemCtrl', {
$scope: scope,
ItemCollection: ItemCollection
});
});
For Item, the method isSomething should take care of checking if Item is undefined before doing Item.attachedItem
Testing an aync block is tricky. someItem returns a promise. $q an angular service to which can be used create async functions while testing.
We need to resolve the deferred object to test the async task.
var ItemCollection, ItemCrtl, deferedObj;
beforeEach(inject(function($controller, $rootScope, $q) {
$scope = $rootScope.$new();
deferedObj = $q.defer();
ItemCollection = jasmine.createSpyObj('ItemCollection', ['someItem']);
ItemCollection.someItem.andReturn(deferedObj.promise);
ItemCtrl = $controller('ItemCtrl', {
$scope: scope,
ItemCollection: ItemCollection
});
});
it('sets page url', function() {
deferedObj.resolve({ pageUrl: 'http://url', someItems: [1,2,3] });
scope.$apply();
expect(ItemCtrl.pageUrl).toEqual('http://url');
});
you have to use mock Item data in test like this (assuming attachedItem value is boolean)
var item={attachedItem:true}
scope.isSomething(item)
$scope.isSomething = function (Item) {
if(!Item.attachedItem){
Item.attachedItem=YOUR_MOCK_VALUE;
}
return ItemCollection.someItem(Item.attachedItem);
};

Angular test Type Error

Have tried a test case in karma, Mocha chai and sinon.
Am getting a error once I use the service. This is my error. Please any help.
AssertionError: expected undefined to deeply equal 'strong'
at /var/www/html/Testing/mocha/node_modules/chai/chai.js:210
at assertEql (/var/www/html/Testing/mocha/node_modules/chai/chai.js:784)
at /var/www/html/Testing/mocha/node_modules/chai/chai.js:3854
at /var/www/html/Testing/mocha/www/index-controller.test.js:22
PhantomJS 1.9.8 (Linux 0.0.0): Executed 1 of 1 (1 FAILED) ERROR (0.043 secs / 0.002 secs)
This is my indexcontroller.js
'use strict';
angular.module('beatso.index-controller', [])
.controller('IndexController', function(
commanService) {
(function(vm){
angular.extend(vm, {
checkPassword: checkPassword
})
vm.headingTop = "Beatso A Music Fanatic Group";
vm.password = "verystrogpassword";
function checkPassword() {
return commanService.passwordValidator("vm.password");
}
})(this);
});
This is my test for indexcontroller.
indeccontroller.test.js
describe('Index Controller', function() {
var indexController;
var commanServiceMock;
var commanService;
beforeEach(module('beatso.index-controller'));
beforeEach(module(initMocks));
beforeEach(inject(initIndexController));
it('should return strong if password length is greater than equal to 8', function() {
expect(indexController.checkPassword()).to.eql('strong');
expect(commanServiceMock.passwordValidator.calledOnce).to.eql(true);
});
function initMocks ($provide){
commanServiceMock = {
passwordValidator: sinon.spy()
};
$provide.service('commanService', function(){
return commanServiceMock;
})
}
function initIndexController($controller) {
indexController = $controller('IndexController');
}
});
This is my common service
'use strict';
angular.module('beatso-comman.service', [])
.factory('commanService', function(){
var service = {
passwordValidator: passwordValidator
}
function passwordValidator(password){
if(password.length >= 8) {
return 'strong'
}else {
return 'weak'
}
}
return service;
})
Here is my test for the service.
'use strict'
describe('Test for my comman service', function(){
var cService;
beforeEach(module('beatso-comman.service'));
beforeEach(inject(initCommanService));
it('It should check the password strength', function(){
expect(cService.passwordValidator('amtoverystrongpassword')).to.eql('strong');
});
function initCommanService(commanService){
cService = commanService;
}
})
Your commanService mock has no method "passwordValidator", so trying to call it raise an "undefined" error.
If you do want to test your service, you should not mock it but actually really test it. You can get a reference to your service by injecting it (see inject() function in Jasmine).
Here's a piece of code from one of my project:
// inject the service itself
beforeEach(inject(function(nfcService){
service = nfcService;
}));
Where, obviously, "service" is the variable I am using to perform my unit tests (and really test my service).
Edit - details:
What I mean above is, the tests of your controller should not test your service... The tests of your controller should test your controller. It should, eventually, using a mock of your service (with a spy on the desired method), check that the appropriate method has been called.
For instance:
myServiceMock = {
expectedMethod: jasmine.createSpy('expectedMethod spy')
}
And in your test:
expect(myServiceMock.expectedMethod).toHaveBeenCalled();
When instantiating a controller with $controller, you can pass it (in a second parameter) an object literal providing its dependencies. This way, you can give it the mock you want.
An example, still from my project:
menuCtrl = $controller('MenuController', {
// where 'uiFeedbackService' is the name of the dependency
'uiFeedbackService': uiFeedbackServiceMock
});
Note: Regarding the declaration of your service, you can directly return an Object literal instead of creating a variable, declaring a private function (passwordValidator), and then returning the variable.
angular.module('beatso-comman.service', [])
.factory('commanService', function(){
return {
passwordValidator: function(password){
if(password.length >= 8) {
return 'strong'
}else {
return 'weak'
}
}
}
})

Testing the controller passed to an Angular Material Dialog instance

First off, I am trying to unit test the controller that is being passed to an Angular Material Dialog instance.
As a general question, does it make more sense to test such a controller separately, or by actually invoking$mdDialog.show()?
I am attempting the first method, but I'm running into some issues, mostly related to how Angular Material binds the "locals" to the controller.
Here is the code that I am using to invoke the dialog in my source code, which works as expected:
$mdDialog.show({
controller: 'DeviceDetailController',
controllerAs: 'vm',
locals: {deviceId: "123"},
bindToController: true,
templateUrl: 'admin/views/deviceDetail.html',
parent: angular.element(document.body),
targetEvent: event
});
I don't believe the docs have been updated, but as of version 0.9.0 or so, the locals are available to the controller at the time the constructor function is called (see this issue on Github). Here is a stripped-down version of the controller constructor function under test, so you can see why I need the variable to be passed in and available when the controller is "instantiated":
function DeviceDetailController(devicesService) {
var vm = this;
vm.device = {};
// vm.deviceId = null; //this field is injected when the dialog is created, if there is one. For some reason I can't pre-assign it to null.
activate();
//////////
function activate() {
if (vm.deviceId != null) {
loadDevice();
}
}
function loadDevice() {
devicesService.getDeviceById(vm.deviceId)
.then(function(data) {
vm.device = data.collection;
};
}
}
I am trying to test that the device is assigned to vm.device when a deviceId is passed in to the constructor function before it is invoked.
The test (jasmine and sinon, run by karma):
describe('DeviceDetailController', function() {
var $controllerConstructor, scope, mockDevicesService;
beforeEach(module("admin"));
beforeEach(inject(function ($controller, $rootScope) {
mockDevicesService = sinon.stub({
getDeviceById: function () {}
});
$controllerConstructor = $controller;
scope = $rootScope.$new();
}));
it('should get a device from devicesService if passed a deviceId', function() {
var mockDeviceId = 3;
var mockDevice = {onlyIWouldHaveThis: true};
var mockDeviceResponse = {collection: [mockDevice]};
var mockDevicePromise = {
then: function (cb) {
cb(mockDeviceResponse);
}
};
var mockLocals = {deviceId: mockDeviceId, $scope: scope};
mockDevicesService.getDeviceById.returns(mockDevicePromise);
var ctrlConstructor = $controllerConstructor('DeviceDetailController as vm', mockLocals, true);
angular.extend(ctrlConstructor.instance, mockLocals);
ctrlConstructor();
expect(scope.vm.deviceId).toBe(mockDeviceId);
expect(scope.vm.device).toEqual(mockDevice);
});
});
When I run this, the first assertion passes and the second one fails ("Expected Object({ }) to equal Object({ onlyIWouldHaveThis: true })."), which shows me that deviceId is being injected into the controller's scope, but apparently not in time for the if clause in the activate() method to see it.
You will notice that I am trying to mimic the basic procedure that Angular Material uses by calling $controller() with the third argument set to 'true', which causes $controller() to return the controller constructor function, as opposed to the resulting controller. I should then be able to extend the constructor with my local variables (just as Angular Material does in the code linked to above), and then invoke the constructor function to instantiate the controller.
I have tried a number of things, including passing an isolate scope to the controller by calling $rootScope.$new(true), to no effect (I actually can't say I fully understand isolate scope, but $mdDialog uses it by default).
Any help is appreciated!
The first thing I would try would be to lose the 'as vm' from your call to $controller. You can just use the return value for your expect rather than testing scope.
Try this:
var ctrlConstructor = $controllerConstructor('DeviceDetailController', mockLocals, true);
angular.extend(ctrlConstructor.instance, mockLocals);
var vm = ctrlConstructor();
expect(vm.deviceId).toBe(mockDeviceId);
expect(vm.device).toEqual(mockDevice);

How to unit test custom decorator with Jasmine (Angular js)

So I have such decorator in app config:
angular.module('app').config(['$provide', function ($provide) {
$provide.decorator('$rootScope', ['$delegate', function ($delegate) {
$delegate.constructor.prototype.$onRootScope = function (name, listener) {
var unsubscribe = $delegate.$on(name, listener);
this.$on('$destroy', unsubscribe);
};
$delegate.constructor.prototype.$watchRootScope = function (name, listener) {
var unsubscribe = $delegate.$watch(name, listener);
this.$on('$destroy', unsubscribe);
};
$delegate.constructor.prototype.$watchAfterLoad = function (watchExpression, listener, objectEquality) {
var initialLoad = true;
this.$watch(watchExpression, function () {
if (initialLoad) {
// note: this obviously runs outside of angular, so sometimes the timeout could run after initial load
setTimeout(function () { initialLoad = false; }, 25);
} else {
listener.apply(this, arguments);
}
}, objectEquality);
};
return $delegate;
}]);
}]);
As you can see this decorator lets me use $scope.$onRootScope instead of $rootScope.$on and takes care of automatic listeners removal on scope destroy event...
When I unit test my code which logic contains $scope.$onRootScope I'm getting such error: TypeError: undefined is not a constructor (evaluating 'scope.$onRootScope') in
Before each test I'm loading all required models and do inject which looks like this ~:
beforeEach(function () {
inject(function (_$rootScope_) {
$rootScope = _$rootScope_;
});
});
How should I overcome this problem?
Is there a way to mock / mimic $scope.$onRootScope behaviour?
I'm quite new to unit testing & Jasmine so sorry for not very nicely formatted question.
EDIT #1:
As I'm mocking my $scope object (var $scope = {...}) before passing it as argument to service method which I'm testing I can avoid error by simply defining $scope method:
$scope = {
...
$onRootScope: function() {}
}
Still awaiting for some better ideas :-)
I believe you need to build your $scope based off of the decorated $rootScope, as opposed to creating a new dummy object.
Like so:
var $root, $scope;
beforeEach(function () {
module('app');
inject(function ($rootScope) {
$root = $rootScope;
$scope = $root.$new();
});
});
it('should have the expected property', function () {
expect($scope.constructor.prototype).to.have.property('$watchRootScope');
});
I'll chuck in a link to the spec suite of a mini-lib I put together some time ago, doing roughly the same thing you are now.

Categories

Resources