I would like to test myService initialisation which could be different depends on conditions:
service('myService', function(internalService){
if(internalService.param){ init1(); }
else { init2(); }
//...
});
I can mock internalService, but how to recreate myService?
Services are instantiated by the injector (inject(...)) after the configuration phase. To test your initialization, all you need to do is setup differently in each tests. That is, rather than instantiating your service in a beforeEach block, do it in the test.
For instance, you could test the side effects of instantiating with init1 by mocking the internalService to have param: true
describe('Unit: myService', function() {
it('should perform init1 if internalService.param', function() {
var mockInternalService = {
param: true,
};
// config phase (mocking)
module('app', function($provide) {
$provide.constant('internalService', mockInternalService);
});
inject(function($injector) {
myService = $injector.get('myService');
// assert side effects from init1();
});
});
});
To test the side effects of init2, just mock the internalService to param: false
it('should perform init2 if !internalService.param', function() {
var mockInternalService = {
param: false,
};
// config phase (mocking)
module('app', function($provide) {
$provide.constant('internalService', mockInternalService);
});
inject(function($injector) {
myService = $injector.get('myService');
// assert side effects from init2();
});
});
EDIT: Of course, if you want to make multiple tests for each configuration, you can create two describe blocks with different beforeEach.
describe('Unit: myService', function() {
var myService;
describe('When instantiated with `init1`', function() {
beforeEach(config(true));
beforeEach(inject(injections));
it('should do X');
});
describe('When instantiated with `init2`', function() {
beforeEach(config(false));
beforeEach(inject(injections));
it('should do Y');
});
function config(param) {
return function() {
var mockInternalService = {
param: param,
};
// config phase (mocking)
module('app', function($provide) {
$provide.constant('internalService', mockInternalService);
});
};
}
function injections($injector) {
myService = $injector.get('myService');
}
});
Related
I am trying to unit test my controller. The function that I am trying to unit test is:
function myFunction() {
MyService
.myMethod(thing1, thing2)
.then(function handleMyMethod(result) {
SomeModule.errorHandler(result)
.onSuccess(function onSuccess() {
// do stuff
})
.onError(function onError() {
// do stuff
});
});
}
Relevant test file snippet:
var MockService = {
myMethod: function(thing1, thing2) {
var promise = $q.defer().promise;
return promise;
}
};
beforeEach(module(function($provide) {
$provide.value('MyService', MockService);
}));
beforeEach(inject(function (_$controller_, _MyService_, _SomeModule_, ...) {
...
MyService = _MyService_;
MyController = _$controller_('MyController as Ctrl', {
$controller: controller,
MyService: MockService,
});
I am confused about how to write tests that allow me to hit both the onSuccess and onError cases. I am trying to cover both branches for branch coverage, but don't know how the syntax works.
You can do it one of two ways:
You can write your mock service to look at the parameters and resolve with an error or success.
myMethod:function(thing1, thing2) {
if (thing1=='all good') return $q.when('excellent');
return $q.reject('sorry, bud');
}
You can override the method closer to where you're calling it.
it('is a success', function() {
spyOn(MockService, 'myMethod').and.returnValue($q.when('excellent');
$rootScope.$apply(
MyController.doStuff('foo');
);
expect(MyController.someProperty).toEqual('excellent');
});
//etc.
Note you don't need to both override the module injector with the provide code and provide the mock service in the $controller locals parameter.
I have a simple enough function that closes an $mdSidenav instance in my application
function closeSideNav() {
$mdSidenav('left').close();
}
I'm now needing to unit test this, but am having trouble writing an expectation for the close() call on $mdSidenav.
I thought about using $provide in my test spec
module(function($provide) {
$provide.value('$mdSidenav', function(id) {
return {
close: jasmine.createSpy('$mdSidenav.close')
}
})
});
beforeEach(inject(function(_$controller_, _$mdSidenav_) {
$controller = _$controller_;
$mdSidenav = _$mdSidenav_;
}));
beforeEach(function() {
vm = $controller('NavbarController', {
$mdSidenav: $mdSidenav
});
});
describe('vm.closeSideNav', function() {
beforeEach(function() {
spyOn($mdSidenav, 'close');
vm.closeSideNav()
});
it('should call $mdSidenav.close()', function() {
expect($mdSidenav.close).toHaveBeenCalled();
});
});
This throws a couple of errors:
Error: close() method does not exist
Error: Expected a spy, but got undefined.
Has anyone managed to mock out $mdSidenav and offer me some guidance please?
Thanks
UPDATE
Based on the suggested answer, I have now updated my test spec to
'use strict';
describe('NavbarController', function() {
var $controller,
vm,
$mdSidenav,
sideNavCloseMock;
beforeEach(function() {
module('app.layout');
sideNavCloseMock = jasmine.createSpy();
module(function($provide) {
$provide.value('$mdSidenav', function() {
return function(sideNavId) {
return {close: sideNavCloseMock}
}
})
});
});
beforeEach(inject(function(_$controller_, _$mdSidenav_) {
$controller = _$controller_;
$mdSidenav = _$mdSidenav_;
}));
beforeEach(function() {
vm = $controller('NavbarController', {
$mdSidenav: $mdSidenav
});
});
describe('vm.closeSideNav', function() {
beforeEach(function() {
vm.closeSideNav()
});
it('should call $mdSidenav.close()', function() {
expect(sideNavCloseMock).toHaveBeenCalled();
});
});
});
And for a sanity check, my actual controller looks as follows:
(function () {
'use strict';
angular
.module('app.layout')
.controller('NavbarController', Controller);
Controller.$inject = ['$mdSidenav'];
function Controller($mdSidenav) {
var vm = this;
vm.closeSideNav = closeSideNav;
//This only affects the sideNav when its not locked into position, so only on small\medium screens
function closeSideNav() {
$mdSidenav('left').close();
}
}
})();
Unfortunately this still isn't working for me, and I end up with a different error
TypeError: undefined is not a constructor (evaluating '$mdSidenav('left').close())
close method doesn't belong to $mdSidenav. $mdSidenav is a function that returns a side nav object. That's why it complains 'close() method does not exist'.
What you can do is mock the $mdSidenav to return an object hat has mocked close method, like this: -
var sideNavCloseMock;
beforeEach(module(function($provide){
sideNavCloseMock = jasmine.createSpy();
$provide.factory('$mdSidenav', function() {
return function(sideNavId){
return {close: sideNavCloseMock};
};
});
}));
then do
it('should call $mdSidenav.close()', function() {
expect(sideNavCloseMock).toHaveBeenCalled();
});
I have the following function that I would like to spy... but it contains a promise... But I am getting TypeError: 'undefined' is not an object (evaluating 'modalService.showModal({}, modalOptions).then')
Because of course I have just spyOn(modalService,'showModal')
How do I account for the promise too so ??
_modalService = {
close: function (value) { console.log(value) },
dismiss: function (value) { console.log(value) },
showModal: function (value) { console.log(value) }
};
spyOn(_modalService, 'close');
spyOn(_modalService, 'dismiss');
spyOn(_modalService, 'showModal');
Controller function:
user.resetPassword = function () {
var modalOptions = {
closeButtonText: 'Cancel',
actionButtonText: 'Reset',
headerText: 'Reset Password',
bodyText: 'Are you sure you want to reset the users password?'
};
modalService.showModal({}, modalOptions).then(function (result) {
if (result === 'ok') {
userDataService.resetPassword(user.data).then(function (result) {
$scope.$emit('showSuccessReset');
});
};
});
};
Here is my unit test:
it('should allow the users password to be reset', function () {
var controller = createController();
controller.resetPassword();
$httpBackend.flush();
})
*******************UPDATE
So I change it to this:
//Create a fake instance of the modal instance. TO ensure that the close is called
_modalService = {
close: function (value) { console.log(value) },
dismiss: function (value) { console.log(value) },
showModal: function (value) { console.log(value) }
};
spyOn(_modalService, 'close');
spyOn(_modalService, 'dismiss');
spyOn(_modalService, 'showModal').and.callThrough();
_modalService.showModal = function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
};
To be honest though I am not sure I could explain this. While I understand all the async stuff... I am not sure how jasmine is using this to make it all work. Can anyone explain the flow???? Also I feel the syntax is wrong... how would you typically write this so it looks better/cleaner...??
When you need to mock a function that returns a promise, you have two options:
Return a mocked promise (an object that resembles a promise);
Return a real promise.
I suggest #2 because it's easier and you don't have to worry about replicating the whole promise API. In other words, it isn't worth mocking a promise itself.
Now about Jasmine: you only need to use spyOn when you already have an object (not a mock) and you want to spy on (no pun intended) one of its methods. In your case, your whole object is fake, so you could use jasmine.createSpyObj instead.
The following example should make all of the above clearer:
SUT
app.controller('MainCtrl', function($scope, modal, service) {
$scope.click = function() {
modal.show().then(function(result) {
if (result === 'ok') {
service.resetPassword();
}
});
};
});
Test
describe('Testing a controller', function() {
var $scope, $q,
ctrl, modalMock, serviceMock;
beforeEach(function() {
module('plunker');
modalMock = jasmine.createSpyObj('modal', ['show']);
serviceMock = jasmine.createSpyObj('service', ['resetPassword']);
inject(function($rootScope, $controller, _$q_) {
$scope = $rootScope.$new();
$q = _$q_;
ctrl = $controller('MainCtrl', {
$scope: $scope,
modal: modalMock,
service: serviceMock
});
});
});
it('should reset the password when the user confirms', function() {
// Arrange
var deferred = $q.defer();
deferred.resolve('ok');
modalMock.show.and.returnValue(deferred.promise);
// Act
$scope.click();
$scope.$digest(); // Makes Angular resolve the promise
// Assert
expect(serviceMock.resetPassword).toHaveBeenCalled();
});
it('should not reset the password when the user cancels', function() {
// Arrange
var deferred = $q.defer();
deferred.resolve('cancel');
modalMock.show.and.returnValue(deferred.promise);
// Act
$scope.click();
$scope.$digest(); // Makes Angular resolve the promise
// Assert
expect(serviceMock.resetPassword).not.toHaveBeenCalled();
});
});
Working Plunker
That mock arrangement code within each test could be moved into a beforeEach section so it doesn't get duplicated. I didn't do that in order to make things simple.
Given the app startup:
angular.module("starter", [ "ionic" ])
.constant("DEBUG", true)
.run(function() {
/* ... */
});
how would I test the value of DEBUG?
When trying with:
describe("app", function() {
beforeEach(function() {
module("starter");
});
describe("constants", function() {
describe("DEBUG", inject(function(DEBUG) {
it("should be a boolean", function() {
expect(typeof DEBUG).toBe("boolean");
});
}));
});
});
I just get
TypeError: 'null' is not an object (evaluating 'currentSpec.$modules')
at workFn (/%%%/www/lib/angular-mocks/angular-mocks.js:2230)
at /%%%/www/js/app_test.js:14
at /%%%/www/js/app_test.js:15
at /%%%/www/js/app_test.js:16
Make sure it is being instantiated in the right place.
In this case, the beforeEach was not being run to load the module, because DEBUG was being inject()ed in the describe block, not the it block. The following works properly:
describe("app", function() {
var DEBUG;
beforeEach(function() {
module("starter");
});
describe("constants", function() {
describe("DEBUG", function() {
it("should be a boolean", inject(function(DEBUG) {
expect(typeof DEBUG).toBe("boolean");
}));
});
});
});
Simple way to inject your existing constants into your karma tests.
// Assuming your constant already exists
angular.module('app').constant('STACK', 'overflow')...
// Your Karma Test Suite
describe('app', function() {
var STACK;
beforeEach(module('APP'));
beforeEach(inject(function ($injector) {
STACK = $injector.get('STACK');
}));
// Tests...
});
So I have two services:
// The service I'm testing
angular.module("m").service("myService", function(otherService) { ... })
// the service I'd like to mock while testing
angular.module("m").service("otherService", function() { ... })
describe("my test", function() {
var myService = null;
beforeEach(module('m'));
beforeEach(inject(function($injector) {
///////////////////////////////////////////////////////
// but I want it to get injected with 'otherService'
///////////////////////////////////////////////////////
myService = $injector.get("myService")
})
it ('test myService', function() {
})
})
I want to mock out otherService before it's injected into myService and I test the instance of myService in follow up it functions.
You should use the $provide service to replace the otherService implementation with a mocked one. Here you go:
describe('my test', function() {
var myService, otherServiceMock;
beforeEach(function() {
module('m');
otherServiceMock = jasmine.createSpyObj('otherService', [...]);
module(function($provide) {
// Replaces the service with a mock object
$provide.value('otherService', otherServiceMock);
});
inject(function(_myService_) {
myService = _myService_;
});
});
});
Check out the $provide documentation for more information.
You can just mock the methods of the service in question on the fly
var myService, otherService;
beforeEach(inject(function($injector) {
myService = $injector.get('myService');
otherService = $injector.get('otherService');
}));
it('calls otherService.doOther when doSomething is called', function() {
spyOn(otherService, 'doOther');
myService.doSomething();
expect(otherService.doOther).toHaveBeenCalled();
});
With the jasmine spies, you can for example test outcomes with different return values, etc.
it('doesSomething returns true when otherService.doOther returns false', function() {
spyOn(otherService, 'doOther').andReturn(false);
expect(myService.doSomething()).toBeTruthy();
});