Bernardo Bernardo - 4 months ago 33
AngularJS Question

Angular JS Nested Services Unit Testing

I spent all afternoon trying to write a Unit Test for a service that is working as expected, without success. The problem comes from the dependencies injected, because I have another service that is passing the unit tests (it doesn't have dependencies).

The service is this



import angular from 'angular';

export class IPMAService {
/*@ngInject*/
constructor($http, localStorageService, appConfig, locationStorageKey) {
debugger;
this.$http = $http;
this.localStorage = localStorageService;
this.appConfig = appConfig;
this.locationStorageKey = locationStorageKey;
if (Object.keys(this.localStorage.get(locationStorageKey)).length < 1) {
console.log("IPMAService - Initialization (locations not present in local storage)");
this._getLocationsFromService();
}
}

getLocations() {
console.log("IPMAService - Get Locations");
var locations = this.localStorage.get(this.locationStorageKey);
if(!locations){
return this._getLocationsFromService();
}
return locations;
};

_getLocationsFromService(){
var _self = this;
this.$http({
method: 'GET',
url: this.appConfig.ipmaWebServices.locations
}).then((response) => {
_self.localStorage.set(_self.locationStorageKey, response.data);
console.log("IPMAService - Saved locations.json to local storage with key: " + _self.locationStorageKey);
return response;
})
.catch((response) => {
console.log("IPMAService - Erro no acesso à api do IPMA: " + _self.appConfig.ipmaWebServices.locations);
})
}
}

export default angular.module('services.ipmaService', [])
.service('ipmaService', IPMAService)
.name;





I read a lot about Unit Tests and I can't seem to get it to work. This was my last attempt (not even a basic .toBeDefined(), given that it crashes in the inject function: "TypeError: Cannot convert undefined or null to object"):



'use strict';

import serviceModule from './IPMAService.service';
import LocalStorageModule from 'angular-local-storage';
import constantsModule from '../../app/app.constants';

describe('Service: IPMAService', function () {
beforeEach(angular.mock.module(serviceModule));
beforeEach(angular.mock.module(LocalStorageModule));
beforeEach(angular.mock.module(constantsModule));

var localStorageService2, service, httpBackend, ipmawebservices, locationStoreKey2;

beforeEach(inject(function (ipmaService, _$http_, localStorageService, appConfig, locationStoreKey) {
service = ipmaService;
httpBackend = _$http_;
localStorageService2 = localStorageService;
ipmawebservices = appConfig;
locationStoreKey2 = locationStorageKey;

}));

it ('should be loaded', function() {
expect(service).toBeDefined();
});
});





Also...after I get this basic test to pass/work, I would like to know how can I, for example, test the getLocations method since it depends on the _getLocationsFromService method and localStorage (I think I have to use jasmine's SpyOn, but a little help would be great).

Thanks in advance.

EDIT

This was my final solution mixing the accepted answer and some more research. Hope it helps someone in the future who's desperate about Unit Tests.

NOTE: I removed the locationStorageKey dependency from the service



'use strict';

import serviceModule, { IPMAService } from './IPMAService.service';
import LocalStorageModule from 'angular-local-storage';
import constantsModule from '../../app/app.constants';

describe('Service: IPMAService', function () {
var mockDependency, appConfigDependency, service,
locationsObject = [{ local: "Lisboa", latitude: 33, longitude: 10 },{ local: "Porto", latitude: 36, longitude: 9 }];
beforeEach(angular.mock.module(serviceModule));

beforeEach(function () {

mockDependency = {
get: function () {
return locationsObject;
}
};

appConfigDependency = {

};

angular.mock.module(function ($provide) {
$provide.value('localStorageService', mockDependency);
});

angular.mock.module(function ($provide) {
$provide.constant('appConfig', appConfigDependency);
});

});

beforeEach(() => {
spyOn(IPMAService.prototype, '_getLocationsFromService').and.returnValue(locationsObject);
});

it('should return location object', inject(function (ipmaService) {
expect(ipmaService.getLocations()).toBe(locationsObject);
}));


});




Answer

The question contains a catch. Nested services shouldn't be tested. Unit testing is about testing a unit, one at a time. One service (ipmaService) is tested, the others are mocked.

This list

beforeEach(angular.mock.module(serviceModule));
beforeEach(angular.mock.module(LocalStorageModule));
beforeEach(angular.mock.module(constantsModule));

should become

var localStorageServiceMock = { get: jasmine.createSpy().and.returnValue(...) };

beforeEach(angular.mock.module(serviceModule, {
  localStorageService: localStorageServiceMock,
  appConfig: ...,
  locationStorageKey: ...
}));

Fortunately, we have a separate class export with ES6 modules alongside with default:

import serviceModule, { IPMAService } from './IPMAService.service';

This gives us the opportunity to set up spies on class prototype methods before the service is instantiated with inject:

beforeEach(() => {
 spyOn(IPMAService.prototype, '_getLocationsFromService').and.callThrough();
});
...
expect(service._getLocationsFromService).toHaveBeenCalled();

Alternatively, the method can be mocked in some service specs to improve granularity and focus on tested methods.

Spying on service's own method in constructor isn't a big deal but it adds the strength to specs.