I'm banging my head around testing a service which uses Google Maps Geoencoding service. Thought it was going to be easy since it's really straightforward code. But seems it's not.
Here is the service:
(function () {
'use strict';
var GoogleGeocodingService = function ($q, GoogleAPILoaderService, $rootScope) {
var geocoder,
mapsReadyPromise;
mapsReadyPromise = GoogleAPILoaderService.load('maps', '3', {other_params: 'sensor=false'}).then(function() {
geocoder = new google.maps.Geocoder();
});
var getLatLng = function (searchKeyword) {
var deferred = $q.defer();
mapsReadyPromise.then(function () {
geocoder.geocode({'address': searchKeyword}, function (results, status) {
$rootScope.$apply(function () {
if (status === google.maps.GeocoderStatus.OK) {
deferred.resolve(results);
} else {
deferred.reject(status);
}
});
});
});
return deferred.promise;
};
return {
getLatLng: getLatLng
};
};
app.factory('GoogleGeocodingService', ['$q', 'GoogleAPILoaderService', '$rootScope', GoogleGeocodingService]);
}());
In order to not use the real google.maps I'm mocking both the GoogleAPILoaderService and google.maps.
However, when I try to test it, I get into the $digest already in progress. I've tried with safeApply but won't work.
it('Should call geocoder.geocode to retrieve results', function () {
GoogleGeocoding.getLatLng('Canada');
$rootScope.$apply();
expect(GeoCoderMock.prototype.geocode).toHaveBeenCalledWith({ address : 'Canada'});
});
This is the full spec:
(function () {
"use strict";
var GeoCodingOK, GeoCodingError, GeoCoderMock, GoogleAPILoaderMock, $rootScope, $q, $timeout, GoogleGeocoding;
describe('Google Geocoding Service', function () {
beforeEach(angular.mock.module('app', function($provide){
GoogleAPILoaderMock = jasmine.createSpyObj('GoogleAPILoaderService',['load']);
$provide.value('GoogleAPILoaderService',GoogleAPILoaderMock);
}));
beforeEach(inject(function (_$q_,_$rootScope_) {
$q = _$q_;
$rootScope = _$rootScope_;
GoogleAPILoaderMock.load.andCallFake(function () {
var deferred = $q.defer();
deferred.resolve('Library Loaded');
return deferred.promise;
});
}));
beforeEach(inject(function (GoogleGeocodingService) {
GoogleGeocoding = GoogleGeocodingService;
window.google = jasmine.createSpy('google');
window.google.maps = jasmine.createSpy('maps');
window.google.maps.GeocoderStatus = jasmine.createSpy('GeocoderStatus');
window.google.maps.GeocoderStatus.OK = 'OK';
GeoCodingOK = function (params, callback) {
callback({data: 'Fake'}, 'OK');
};
GeoCodingError = function (params, callback) {
callback({data: 'Fake'}, 'ERROR');
};
GeoCoderMock = window.google.maps.Geocoder = jasmine.createSpy('Geocoder');
GeoCoderMock.prototype.geocode = jasmine.createSpy('geocode').andCallFake(GeoCodingOK);
}));
it('Should expose some functions', function(){
expect(typeof GoogleGeocoding.getLatLng).toBe('function');
});
describe('getLatLng function', function () {
it('Shouldn\'t call anything if the promise hasn\'t been resolved', function () {
GoogleGeocoding.getLatLng('Canada');
expect(GeoCoderMock.prototype.geocode).not.toHaveBeenCalled();
});
it('Should return a promise', function () {
var promise = GoogleGeocoding.getLatLng('Canada');
expect(typeof promise.then).toBe('function');
});
it('Should call geocoder.geocode to retrieve results', function () {
GoogleGeocoding.getLatLng('Canada');
$rootScope.$apply();
expect(GeoCoderMock.prototype.geocode).toHaveBeenCalledWith({ address : 'Canada'});
});
it('Should resolve the promise when receiving data', function () {
var okMock = jasmine.createSpy();
GoogleGeocoding.getLatLng('Canada').then(okMock);
$rootScope.$apply();
expect(okMock).toHaveBeenCalledWith({ address : 'Canada'});
});
});
});
}());
FAQ:
Have you tried $$phase checking?
Yes. Doesn't work. Somehow the phase is null at that point. I fear that by calling $apply I'm unleashing two of them some how, which is causing the issue.
Can you provide a Plunker for this?
Yes of course! Link to the Plunker
The problem is simple. The $apply inside the mapsReadyPromise is not needed so when you do another $apply on the test, it gets crazy. Removing that $apply resolved the $digest problem, then you only needed to fix a couple of issues and it is ready :)
http://plnkr.co/edit/wRdJNqAk9RZ7vg3Dli6K?p=preview
Related
I am fairly new to JS and I was having some issues with testing asynch methods with jasmine mocking.
Here is the class I want to test:
test.factory('service', ['$http', '$q', function($http, $q) {
var _func = function(varB, successCallback, errorCallback) {
//Sanity check on input
if (isNaN(varB)) {
errorCallback('varB has to be a number');
return;
}
if (parseInt(varB) < 0) {
errorCallback('varB count has to be positive');
return;
}
$http.get('http://www.test.com').then(function(response) {
var data = angular.copy(response.data);
if (successCallback) {
successCallback(data);
}
}, function(errorResponse) {
if (errorCallback) {
errorCallback(errorResponse.data);
}
});
};
return {
func: function(varB) {
return $q(function(resolve, reject) {
_func(varB, resolve, reject);
});
}
};
}]);
One of the tests:
beforeEach(inject(function($injector) {
service = $injector.get(
'service');
$httpBackend = $injector.get('$httpBackend');
}));
it('should use the rejection handler if varB is in invalid format',
function() {
var successHandler = jasmine.createSpy('success');
var failHandler = jasmine.createSpy('fail');
service.func('abc').then(successHandler, failHandler);
expect(successHandler).not.toHaveBeenCalled();
expect(failHandler).toHaveBeenCalledWith('varB has to be a number');
}
);
The line in the test:
expect(failHandler).toHaveBeenCalledWith('varB has to be a number'); never gets executed for some reason. I even tried putting debug statements in the service class to see if the methods were reached and they were getting reached. But the mock was not able to detect that the callback was called with the given argument.
Would be great to get some insights! Thanks!
Ok I found out the solution.
Got hints here.
So the thing is to add a rootScope injection. It worked after that! :)
beforeEach(inject(function($injector) {
service = $injector.get(
'service');
$httpBackend = $injector.get('$httpBackend');
$rootScope = $injector.get('$rootScope');
}));
it('should use the rejection handler if varB is in invalid format',
function() {
var successHandler = jasmine.createSpy('success');
var failHandler = jasmine.createSpy('fail');
service.func('abc').then(successHandler, failHandler);
$rootScope.$digest();
expect(successHandler).not.toHaveBeenCalled();
expect(failHandler).toHaveBeenCalledWith('varB has to be a number');
}
);
Hi I have a Angular service that uses another service that loads data from the local storage on init.
angular
.module('app')
.factory('localStorage', function ($window)
{
if (!$window.localStorage)
{
// throw Error
}
return $window.localStorage;
});
angular
.module('app')
.factory('session', function (localStorage)
{
var container = JSON.parse(localStorage.getItem('sessionContainer'));
return {
getUser: getUser
};
});
Now i want to test the session service.
describe('SessionService', function ()
{
var service;
var localStorageMock;
// Load the module.
beforeEach(module('appRegistration'));
// Create mocks.
beforeEach(function ()
{
logMock = {};
localStorageMock = jasmine.createSpyObj('localStorageServiceMockSpy', ['setItem', 'getItem']);
localStorageMock.getItem.and.returnValue('{}');
module(function ($provide)
{
$provide.value('localStorage', localStorageMock);
});
inject(function (_session_)
{
service = _session_;
});
});
it('should call `getItem` on the `localStorageService` service', function ()
{
expect(localStorageMock.getItem).toHaveBeenCalledWith('sessionContainer');
});
describe('getUser method', function ()
{
it('should return an empty object when the user is not set', function ()
{
var result = service.getUser();
expect(result).toEqual({});
});
it('should return the user data', function ()
{
// localStorageMock.getItem.and.returnValue('{"user":{"some":"data"}}');
var result = service.getUser();
expect(result).toEqual({some: 'user data'});
});
});
});
As you can see in the should return the user data section.
I need a way to update the container so getUser returns the expected data.
I tried to update the getItem spy, but this does not work. The localStorageMock is already injected in the session service when i want to change the spy.
Any help?
The most simple way is to have a variable with mocked value that is common for both function scopes:
var getItemValue;
beforeEach({
localStorage: {
getItem: jasmine.createSpy().and.callFake(function () {
return getItemValue;
}),
setItem: jasmine.createSpy()
}
});
...
it('should return the user data', function ()
{
getItemValue = '{"user":{"some":"data"}}';
inject(function (_session_) {
service = _session_;
});
var result = service.getUser();
expect(result).toEqual({some: 'user data'});
});
Notice that inject should be moved from beforeEach to it for all specs (the specs that don't involve getItemValue may use shorter syntax, it('...', inject(function (session) { ... }))).
This reveals the flaw in service design that makes it test-unfriendly.
The solution is to make container lazily evaluated, so there is time to mock it after the app was bootstrapped with inject:
.factory('session', function (localStorage)
{
var containerCache;
function getUser() {
...
return this.container;
}
return {
get container() {
return (containerCache === undefined)
? (containerCache = JSON.parse(localStorage.getItem('sessionContainer')))
: containerCache;
},
getUser: getUser
};
});
Additionally, this makes possible to test session.container as well. In this case localStorageMock.getItem spy value may be redefined whenever needed.
I recently started to learn unit test for angular apps. And already faced up with problem. I can not take scope variable from inside executed function. Here is my factory code
angular.module('app').factory('AuthenticationService', AuthenticationService);
AuthenticationService.$inject = ['$http'];
function AuthenticationService($http) {
var service = {};
service.login = login;
return service;
function login(data, callback) {
$http({
method: 'POST',
url: CONFIG.getUrl('auth/login'),
data: data
}).then(function (response) {
callback(response);
}, function (error) {
callback(error);
});
}
Part of my controller file. I only yet wan to test login function
function AuthCtrl($scope, $location, AuthenticationService) {
var vm = this;
vm.login = login;
vm.dataLogin = {
user_id: '',
password: '',
};
function login() {
vm.dataLoading = true;
AuthenticationService.login(vm.dataLogin, function (response) {
if (response.status == 200) {
if (response.data.error_code == 'auth.credentials.invalid') {
vm.invalidCredentials = true;
} else {
vm.invalidCredentials = false;
if (response.data.session_state == 'otp_required') {
vm.userNumber = response.data.user_phone;
$localStorage['session_token'] = response.data.session_token;
vm.needForm = 'someForm';
} else {
AuthenticationService.setCredentials(response.data);
$state.go('dashboard');
}
vm.dataLoading = false;
}
}
});
}
}
});
And my spec.js
describe('AuthCtrl, ', function() {
var $scope, ctrl;
var authSrvMock;
var mockJson = {
user_id: '001',
session_token: 'some_token'
};
var mockLoginData = {
user_id: '0000102',
password: '123456'
};
var mockResponseData = {
data: {
"session_expires": 1453822506,
"session_state": "otp_required",
"session_token": "tokennnn",
"status": "success",
"user_id": "0000102",
"user_phone": "+7 (XXX) XXX-XX-89"
},
status: 200
};
beforeEach(function () {
authSrvMock = jasmine.createSpyObj('AuthenticationService', ['login', 'logout']);
module('app');
inject(function ($rootScope, $controller, $q) {
$scope = $rootScope.$new();
authSrvMock.login.and.returnValue(mockResponseData);
ctrl = $controller('AuthCtrl', {
$scope: $scope,
AuthenticationService: authSrvMock
});
});
});
it('should call login function and pass to dashboard', function () {
ctrl.login();
expect(authSrvMock.login).toHaveBeenCalled();
// until this everything works here just fine
});
});
But after I want to test vm.invalidCredentials, if I will write
expect(ctrl.invalidCredentials).toBe(false)
I will get the error
Expected undefined to be false.
Why I can't see variables?
Bit of a noob myself at Jasmine, but I'm guessing it's because you need to get the promise from your login() to return in Jasmine.
Look into using $q.defer(), or even $httpBackend.
After some more digging process and experiments I found solution.
Here what I did
(function () {
'use strict';
describe('AuthCtrl', function () {
var controller, scope, myService, q, deferred, ctrl;
var mockResponseData = {
response1: {
//...
},
response2: {
//...
},
response3: {
//...
}
};
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope, $q, $httpBackend, AuthenticationService) {
function mockHttp(data, callback) {
deferred = $q.defer();
deferred.promise.then(function (response) {
callback(response);
}, function (error) {
callback(error);
});
}
controller = $controller;
scope = $rootScope.$new();
myService = AuthenticationService;
q = $q;
myService.login = mockHttp;
}));
describe('when returning promises', function () {
beforeEach(function () {
ctrl = controller('AuthCtrl', {
$scope: scope,
myService: myService
});
ctrl.initController();
});
it('shows another form to validate login process', function () {
ctrl.login();
deferred.resolve(mockResponseData.response1);
scope.$digest();
expect(ctrl.invalidCredentials).toBe(false);
expect(ctrl.needForm).toEqual('2sAuth');
expect(ctrl.dataLoading).toBe(false);
});
});
});
})();
Since in my factory almost every method requires data and callback I've created mockHttp functions which takes those arguments and deferred promise. In it block I simply call need function, resolve promise with my prepared answers mock and check my expectations. Everything work. Thanks to for aiming in wich way to look
I have myService that uses myOtherService, which makes a remote call, returning promise:
angular.module('app.myService', ['app.myOtherService'])
.factory('myService', [
myOtherService,
function(myOtherService) {
function makeRemoteCall() {
return myOtherService.makeRemoteCallReturningPromise();
}
return {
makeRemoteCall: makeRemoteCall
};
}
])
To make a unit test for myService I need to mock myOtherService, such that its makeRemoteCallReturningPromise method returns a promise. This is how I do it:
describe('Testing remote call returning promise', function() {
var myService;
var myOtherServiceMock = {};
beforeEach(module('app.myService'));
// I have to inject mock when calling module(),
// and module() should come before any inject()
beforeEach(module(function ($provide) {
$provide.value('myOtherService', myOtherServiceMock);
}));
// However, in order to properly construct my mock
// I need $q, which can give me a promise
beforeEach(inject(function(_myService_, $q){
myService = _myService_;
myOtherServiceMock = {
makeRemoteCallReturningPromise: function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
}
};
}
// Here the value of myOtherServiceMock is not
// updated, and it is still {}
it('can do remote call', inject(function() {
myService.makeRemoteCall() // Error: makeRemoteCall() is not defined on {}
.then(function() {
console.log('Success');
});
}));
As you can see from the above, the definition of my mock depends on $q, which I have to load using inject(). Furthermore, injecting the mock should be happening in module(), which should be coming before inject(). However, the value for the mock is not updated once I change it.
What is the proper way to do this?
I'm not sure why the way you did it doesn't work, but I usually do it with the spyOn function. Something like this:
describe('Testing remote call returning promise', function() {
var myService;
beforeEach(module('app.myService'));
beforeEach(inject( function(_myService_, myOtherService, $q){
myService = _myService_;
spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
});
}
it('can do remote call', inject(function() {
myService.makeRemoteCall()
.then(function() {
console.log('Success');
});
}));
Also remember that you will need to make a $digest call for the then function to be called. See the Testing section of the $q documentation.
------EDIT------
After looking closer at what you're doing, I think I see the problem in your code. In the beforeEach, you're setting myOtherServiceMock to a whole new object. The $provide will never see this reference. You just need to update the existing reference:
beforeEach(inject( function(_myService_, $q){
myService = _myService_;
myOtherServiceMock.makeRemoteCallReturningPromise = function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
};
}
We can also write jasmine's implementation of returning promise directly by spy.
spyOn(myOtherService, "makeRemoteCallReturningPromise").andReturn($q.when({}));
For Jasmine 2:
spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));
(copied from comments, thanks to ccnokes)
describe('testing a method() on a service', function () {
var mock, service
function init(){
return angular.mock.inject(function ($injector,, _serviceUnderTest_) {
mock = $injector.get('service_that_is_being_mocked');;
service = __serviceUnderTest_;
});
}
beforeEach(module('yourApp'));
beforeEach(init());
it('that has a then', function () {
//arrange
var spy= spyOn(mock, 'actionBeingCalled').and.callFake(function () {
return {
then: function (callback) {
return callback({'foo' : "bar"});
}
};
});
//act
var result = service.actionUnderTest(); // does cleverness
//assert
expect(spy).toHaveBeenCalled();
});
});
You can use a stubbing library like sinon to mock your service. You can then return $q.when() as your promise. If your scope object's value comes from the promise result, you will need to call scope.$root.$digest().
var scope, controller, datacontextMock, customer;
beforeEach(function () {
module('app');
inject(function ($rootScope, $controller,common, datacontext) {
scope = $rootScope.$new();
var $q = common.$q;
datacontextMock = sinon.stub(datacontext);
customer = {id:1};
datacontextMock.customer.returns($q.when(customer));
controller = $controller('Index', { $scope: scope });
})
});
it('customer id to be 1.', function () {
scope.$root.$digest();
expect(controller.customer.id).toBe(1);
});
using sinon :
const mockAction = sinon.stub(MyService.prototype,'actionBeingCalled')
.returns(httpPromise(200));
Known that, httpPromise can be :
const httpPromise = (code) => new Promise((resolve, reject) =>
(code >= 200 && code <= 299) ? resolve({ code }) : reject({ code, error:true })
);
Honestly.. you are going about this the wrong way by relying on inject to mock a service instead of module. Also, calling inject in a beforeEach is an anti-pattern as it makes mocking difficult on a per test basis.
Here is how I would do this...
module(function ($provide) {
// By using a decorator we can access $q and stub our method with a promise.
$provide.decorator('myOtherService', function ($delegate, $q) {
$delegate.makeRemoteCallReturningPromise = function () {
var dfd = $q.defer();
dfd.resolve('some value');
return dfd.promise;
};
});
});
Now when you inject your service it will have a properly mocked method for usage.
I found that useful, stabbing service function as sinon.stub().returns($q.when({})):
this.myService = {
myFunction: sinon.stub().returns( $q.when( {} ) )
};
this.scope = $rootScope.$new();
this.angularStubs = {
myService: this.myService,
$scope: this.scope
};
this.ctrl = $controller( require( 'app/bla/bla.controller' ), this.angularStubs );
controller:
this.someMethod = function(someObj) {
myService.myFunction( someObj ).then( function() {
someObj.loaded = 'bla-bla';
}, function() {
// failure
} );
};
and test
const obj = {
field: 'value'
};
this.ctrl.someMethod( obj );
this.scope.$digest();
expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );
The code snippet:
spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
});
Can be written in a more concise form:
spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
return $q.resolve('Remote call result');
});
I have been following the angular testing Play by Play on PluralSight by John Papa and Ward Bell.
I'm currently getting the following error when I run my specs.
AssertionError: expected { Object ($$state) } to have a property 'length'
at Assertion.assertLength (bower_components/chai/chai.js:1331:37)
at Assertion.assert (bower_components/chai/chai.js:4121:49)
at Context.<anonymous> (scripts/home/homeController.Specs.js:48:49)
Note that I have only included the code that I think is relevant so that I am not overloading this question with irrelevant information. If you need to see more code it's not a problem.
My code is as follows:
homeController.js:
window.app.controller('homeController', ['$scope', 'sidebarService',
function ($scope, sidebarService) {
$scope.title = 'Slapdash';
$scope.sidebar = {
"items": sidebarService.getSidebarItems()
};
}])
sidebarService.js:
(function () {
window.app
.service('sidebarService',['$http', function ($http) {
this.getSidebarItems = function () {
$http.get("http://wwww.something.com/getSidebarItems")
.then(function (response) {
return response.data;
});
};
}]);
}());
homeController.Specs.js:
beforeEach:
beforeEach(function () {
bard.appModule('slapdash');
bard.inject(this, '$controller', '$q', '$rootScope')
var mockSidebarService = {
getSidebarItems : function(){
return $q.when(mockSidebarMenuItems);
}
};
controller = $controller('homeController', {
$scope: scope,
sidebarService: mockSidebarService
});
});
failing spec:
it('Should have items', function () {
$rootScope.$apply();
expect(scope.sidebar.items).to.have.length(mockSidebarMenuItems.length); // same number as mocked
expect(sidebarService.getSidebarItems).to.have.been.calledOnce; // it's a spy
});
The answer was that I was returning a result from the service not a promise.
$http.get("http://wwww.something.com/getSidebarItems")
.then(function (response) {
return response.data; // <- returning data not promise
});
When I was mocking I was using
var mockSidebarService = {
getSidebarItems : function(){
return $q.when(mockSidebarMenuItems);
}
};
which mocks a promise. However I just needed to return the data as the promise was awaited in the service.
mockSidebarService = {
getMenuItems : function(){
return mockSidebarMenuItems
}
};
I made the changes and it all works now. Took a while but at least it's making sense.