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.
Related
I have written some service in angular. Check this PLUNKER.
Injecting CommonService, $rootRouter, ModalService in RouteService.
I am stuck with unit testing these services. You can see sample spec file at PLUNKER.
EDIT: Whatever test I have at plunker are not working as expected. Am not sure what I am doing wrong.
How to test goTo and getActivePage methods in RouteService?
How to test getProperty and setProperty methods in CommonService?
Here is code.
First service is RouteService
'use strict';
angular.module('mysampleapp')
.service('RouteService',
function(CommonService, $rootRouter, ModalService) {
console.log('RRRRRRRRRRRRRRRRRRRRRRRRRRRoute');
return {
goTo: goTo,
getActivePage: getActivePage
};
function goTo(page) {
var valid = CommonService.getProperty('isValidationSuccess');
switch (page) {
case 'AboutUs':
if (valid) {
CommonService.setProperty('activeMenu', page);
$rootRouter.navigate([page]);
} else {
ModalService.openModal('Analysis Error', 'Complete Application Group configuration prior to running analysis.', 'Error');
}
break;
default:
CommonService.setProperty('activeMenu', page);
$rootRouter.navigate([page]);
break;
}
}
function getActivePage() {
return CommonService.getProperty('activeMenu');
}
});
Another is CommonService
'use strict';
angular.module('mysampleapp')
.service('CommonService',
function() {
var obj = {
/* All page validation check before perform analysis */
isValidationSuccess: false,
/* Highlight the menu */
activeMenu: 'HomeMenu'
};
function setProperty(key, value) {
obj[key] = value;
}
function getProperty(key) {
return obj[key];
}
function getAllProperties() {
return obj;
}
return {
setProperty: setProperty,
getProperty: getProperty,
getAllProperties: getAllProperties
};
}
);
In your plunker you forgot to create the mysampleapp module before adding services to it:
angular.module('mysampleapp', []);
The test for setters and getters of CommonService should be pretty simple:
describe('CommonService', function () {
var commonService;
beforeEach(module('mysampleapp'));
beforeEach(inject(function (_CommonService_) {
commonService = _CommonService_;
}));
it('should set and get property', function () {
commonService.setProperty('isValidationSuccess', 'Perform');
expect(commonService.getProperty('isValidationSuccess')).toBe('Perform');
});
});
Unit tests for services in most cases should be islolated from other services. If you going to testing CommonService you must mock other services, such as CommonService and etc. Main reason that you do not have to worry how to run for another service, because in this test you expecting that other services will work correctly.
describe('RouteService', function () {
'use strict';
var RouteService,
ModalService,
CommonService,
mockedValue,
$rootRouter;
beforeEach(module('mysampleapp'));
beforeEach(inject(function (_RouteService_, _ModalService_, _CommonService_, _$rootRouter_) {
RouteService = _RouteService_;
ModalService = _ModalService_;
CommonService = _CommonService_;
$rootRouter = _$rootRouter_;
$rootRouter.navigate = jasmine.createSpy();
ModalService.openModal = jasmine.createSpy(); //sometimes open modal return promise, and you should check it to
CommonService.getProperty = jasmine.createSpy().and.callFake(function () {
return mockedValue;
});
CommonService.setProperty = jasmine.createSpy().and.callFake(function () {
return mockedValue;
});
}));
it('should exist', function () {
expect(RouteService).toBeDefined();
});
it('should get active page', function () {
RouteService.getActivePage();
expect(CommonService.getProperty).toHaveBeenCalled(); //this test make sens only for make you coverage 100%, in you case i mean
});
describe('goTo method', function () {
it('should check if it is valid page', function () {
RouteService.goTo();
expect(CommonService.getProperty).toHaveBeenCalled();
});
it('should set property if page is "about as" and if it is valid page, and should navigate to this page', function () {
mockedValue = true;
var page = 'AboutUs';
RouteService.goTo(page);
expect(CommonService.setProperty).toHaveBeenCalledWith('activeMenu', page);
expect($rootRouter.navigate).toHaveBeenCalledWith([page]);
expect(ModalService.openModal).not.toHaveBeenCalled();
});
it('should open modal with error if "about as" is not valid page', function () {
var isValid = mockedValue = false;
var page = 'AboutUs';
RouteService.goTo(page);
expect(ModalService.openModal).toHaveBeenCalled();
expect(CommonService.setProperty).not.toHaveBeenCalled();
expect($rootRouter.navigate).not.toHaveBeenCalled();
});
it('should set property and navigate to page', function () {
var page = 'Test Page';
RouteService.goTo(page);
expect(CommonService.setProperty).toHaveBeenCalledWith('activeMenu', page);
expect($rootRouter.navigate).toHaveBeenCalledWith([page]);
expect(ModalService.openModal).not.toHaveBeenCalled();
});
});
});
I try to test my app with Jasmine and got the following problem:
I will calculate something in the then function of my promise. That's the point where I need to test my code.
Here is the code of my controller:
TestCtrl.$inject = ["$scope", "TestService"];
/* ngInject */
function TestCtrl($scope, TestService) {
$scope.loadData = function () {
TestService.getData().then(function (response) {
$scope.data = response.data;
$scope.filtered = $scope.data.filter(function(item){
if(item.id > 1000){
return true;
}
return false;
})
});
}
}
And my Jasmine test code:
describe('TestService tests', function () {
var $q;
beforeEach(function () {
module('pilot.fw.user');
});
beforeEach(inject(function (_$q_) {
$q = _$q_;
}));
describe('UserController Tests', function () {
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
this.scope = $rootScope.$new();
this.$rootscope = $rootScope;
this.$httpBackend = _$httpBackend_;
this.scope = $rootScope.$new();
var TestServiceMock = {
getData: function () {
var deferred = $q.defer();
var result = [{
"id": 1720,
"user": 1132
},
{
"id": 720,
"user": 132
}, {
"id": 1721,
"user": 1132
}];
deferred.promise.data = result;
deferred.resolve(result);
return deferred.promise;
}
};
this.controller = $controller('TestCtrl', {
'$scope': this.scope,
'TestService': TestServiceMock
});
}));
it('test', function(){
this.scope.loadData();
expect(true).toBeTruthy();
})
});
});
The strange thing I don't understand is (tested with console logs):
My promise is created and returned
My loadData function is called and it will call the getData() function from the TestService
Everything inside the then function won't be executed although I return the promise as resolved
So how could I test the code inside the then function?
Thanks for help
the jasmine 'it' method takes a done parameter that you can call for async testing
it('Should be async', function(done) {
someAsyncFunction().then(function(result) {
expect(result).toBe(true);
done();
});
});
Feel free to go as deep as you want, just be sure to call done when EVERYTHING is finished. Jasmine's default timeout is 5 seconds per test, so if the async stuff isn't done by then jasmine will crash. You can change this setting in the configs or set it in the terminal.
This is straight from the jasmine docs, showing you how to handle the default timeout interval
describe("long asynchronous specs", function() {
var originalTimeout;
beforeEach(function() {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
});
it("takes a long time", function(done) {
setTimeout(function() {
done();
}, 9000);
});
afterEach(function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
});
I think that if it doesn't work in 10 seconds, you may have faulty methods. ESPECIALLY if you are talking to a local server / db. This stuff should only take this long if you are performing HEAVY computations, or are hitting an external api with a not-so-great internet connection. If everything is local (or stubbed / mocked!) then anything over 5-10 seconds is a definite red flag.
You better watch this https://codecraft.tv/courses/angular/unit-testing/asynchronous/
You have actually 3 ways:
1) use regular it:
it('test', (done) => {
const spy = spyOn(func, 'bar').and.returnValue(Promise.resolve(true));
spy.calls.mostRecent().returnValue.then(res => {
...your expect here...
done();
})
} );
2) use async in beforeEach and it:
it('test', async(() => {
spyOn(func, 'bar').and.returnValue(Promise.resolve(true));
fixture.whenStable().then(res => {
...your expect here...
})
} ));
3) use fakeAsync if you don't have Http or XHR calls:
it('test', fakeAsync(() => {
spyOn(func, 'bar').and.returnValue(Promise.resolve(true));
tick();
...your expect here...
} ));
hope this solution helps. One approach I've found useful when testing is mocking dependencies. I've tried to comment out what I've done as much as possible.
var returnMock, $scope, TestServiceMock, controller;
beforeEach(module('app'));
beforeEach(inject(function($controller) {
returnMock = {
then: jasmine.createSpy(),
};
$scope = {};
// first assumption is You are testing TestService extensively,
// I don't care about what getData has to do to get results
// All I care about is it gets called when I call loadData
TestServiceMock = {
getData: jasmine.createSpy().and.returnValue(returnMock);
};
controller = $controller;
}));
it('should load data when loadData function is called and result set is
under 1000', function() {
controller('TestCtrl', {
$scope,
TestServiceMock
});
// another assumption is your data comes back in such a format
// perhaps in the actual code check whether data exists and proceed
// or do some other action
var returnedData = {
data: [
{
id: 1,
name: 'item 1',
},
]
}
// when I execute the function/method
$scope.loadData();
// I expect getData to be called
expect(TestServiceMock.getData).toHaveBeenCalled();
// I expect then to be called and the reason is I mocked it
expect(returnMock.then).toHaveBeenCalledWith(jasmine.any(Function));
returnMock.then.calls.mostRecent().args[0](returnedData);
// expect data on scope to be equal to my mocked data
expect($scope.data).toEqual(returnedData.data);
// don't expect any result because 1 < 1000
expect($scope.filtered).toEqual([]);
expect($scope.filtered.length).toEqual(0);
});
it('should load data when loadData function is called and result set is over 1000',
function() {
controller('TestCtrl', {
$scope,
TestServiceMock
});
var returnedData = {
data: [
{
id: 1,
name: 'item 1',
},
{
id: 1000,
name: 'item 1000',
},
{
id: 1001,
name: 'item 1000',
},
{
id: 1002,
name: 'item 1002',
}
]
}
$scope.loadData();
expect(TestServiceMock.getData).toHaveBeenCalled();
expect(returnMock.then).toHaveBeenCalledWith(jasmine.any(Function));
returnMock.then.calls.mostRecent().args[0](returnedData);
expect($scope.data).toEqual(returnedData.data);
// expect a result because some entries in the mocked data have id > 1000
expect($scope.filtered).toEqual([
{
id: 1001,
name: 'item 1000',
},
{
id: 1002,
name: 'item 1002',
}]);
expect($scope.filtered.length).toEqual(2);
});
Official Jasmine Docs explain most of the concepts extensively. Hope the solution helps!!!!
Let me tell ya what I do, for Angular 1.x and 2.x+ projects. Use the angular testing tools to get rid of callbacks/nests in your async tests. In angular 1.x, that means using a combination of $q and $rootScope.$apply(). In angular 2.x+, that means using something like fakeAsync.
From the Angular 1.x docs
it('should simulate promise', inject(function($q, $rootScope) {
var deferred = $q.defer();
var promise = deferred.promise;
var resolvedValue;
promise.then(function(value) { resolvedValue = value; });
expect(resolvedValue).toBeUndefined();
// Simulate resolving of promise
deferred.resolve(123);
// Note that the 'then' function does not get called synchronously.
// This is because we want the promise API to always be async, whether or not
// it got called synchronously or asynchronously.
expect(resolvedValue).toBeUndefined();
// Propagate promise resolution to 'then' functions using $apply().
$rootScope.$apply();
expect(resolvedValue).toEqual(123);
}));
The disadvantage is that your code is tied to angular, the advantages are that your code is flat and it's portable to 2.x+!
I was a fan of the mocha test runner that allowed me to return promises in my tests, you could try to get that going, but there are downsides to that as well like needing to modify your code specifically for a test.
In regards to your controller, you should 'return' values like so.
TestCtrl.$inject = ["$scope", "TestService"];
/* ngInject */
function TestCtrl($scope, TestService) {
$scope.loadData = function () {
// Return this call, since it will return a new promise
// This is what let's you do $scope.loadData.then()
return TestService.getData().then(function (response) {
// What you return in here will be the first argument
// of your then method, in the tests / any env
// Ex. return 'foo'
// will result in .then(result => result === 'foo') //=> true
// return one of these, i suggest the data, go SRP!
return $scope.data = response.data;
// I would do this stuff in a separate function, but you
// can return 'filtered' instead if you like.
//
// $scope.filtered = $scope.data.filter(function(item){
// if(item.id > 1000){
// return true;
// }
// return false;
// });
});
}
}
Remember that calling something AFTER 'then' doesn't mean anything, values must be called INSIDE 'then'. Not after it, or before it. But inside it. Like Tom Green and that poor moose in Freddy Got Fingered.
I have an Angular controller, which appeared to be working fine. I can console log the user variable inside of the service call, and it contains the correct data. However in my test, I can console log the controller and verify the user object is there, but it is empty. It really seems like initialize is trying to store the variable after the local scope is destroyed, but it is very strange as I have another controller & test written in the exact same way working fine.
I have been iterating over this for two days, so if anyone has any leads, I would be most grateful.
function DetailAccountController (accountsService) {
'use strict';
var user = {};
initialize();
return {
user: user
};
/**
* Initialize the controller,
* & fetch detail for a single user.
*/
function initialize () {
// If the service is available, then fetch the user
accountsService && accountsService.getById('').then(function (res) {
user = res;
});
}
}
and a jasmine test:
describe('DetailAccountController', function () {
var ctrl = require('./detail-account-controller'),
data = [{
"email": "fakeUser0#gmail.com",
"voornaam": "Mr Fake0",
"tussenvoegsel": "van0",
"achternaam": "User0",
"straat": "Mt Lincolnweg0",
"huisnr": 0,
"huisnr_toev": 0,
"postcode": "0LW",
"telefoonr": "0200000000",
"mobielnr": "0680000000",
"plaats": "Amsterdam",
"id": "00000000"
}],
accountsServiceMock,
$rootScope,
$q;
beforeEach(inject(function (_$q_, _$rootScope_) {
$q = _$q_;
$rootScope = _$rootScope_;
accountsServiceMock = {
getById: function () {}
};
}));
it('should call the getById method at least once', function () {
spyOn(accountsServiceMock, 'getById').and.returnValue($q.defer().promise);
ctrl.call({}, accountsServiceMock);
expect(accountsServiceMock.getById.calls.any()).toBe(true);
expect(accountsServiceMock.getById.calls.count()).toBe(1);
});
it('should populate user data in the model', function () {
var deferred = $q.defer();
deferred.resolve(data);
spyOn(accountsServiceMock, 'getById').and.returnValue(deferred.promise);
var vm = ctrl.call({}, accountsServiceMock);
$rootScope.$apply();
expect(vm.user).toEqual(data);
});
});
Updated solution for the curious
function DetailAccountController (accountsService) {
'use strict';
var self = this;
self.user = null;
initialize();
return self;
/**
* Initialize the controller,
* & fetch detail for a single user.
*/
function initialize () {
accountsService && accountsService.getById('').then(function (res) {
self.user = res;
});
}
}
user = res affects local variable and has nothing to do with returned object.
It has to be either
accountsService && accountsService.getById('').then(function (res) {
angular.extend(user, res);
});
or
var obj = {
user: {}
};
initialize();
return obj;
function initialize () {
accountsService && accountsService.getById('').then(function (res) {
obj.user = res;
});
}
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 a unit test for an Angular service in which I test that a cache $cacheFactory is cleared after a call has been made for a save() method that does an http post to the backend. In 1.0.7 this test passed in Karma and Jasmine Specrunner.html, but after migrating to Angular 1.2.0 it fails. I have not changed any code in the service or in the spec file. The cache is cleared in production when I check it manually. Any ideas?
EDIT: Plunk of the error in action: http://plnkr.co/edit/1INhdM
The error message is:
Field service save() should clear field array from cache.
Expected 2 to be 1.
Error: Expected 2 to be 1.
at new jasmine.ExpectationResult (http://localhost:1234/js/test/lib/jasmine/jasmine.js:114:32)
at null.toBe (http://localhost:1234/js/test/lib/jasmine/jasmine.js:1235:29)
at http://localhost:1234/js/test/spec/field-serviceSpec.js:121:25
at wrappedCallback (http://localhost:1234/js/angular-1.2.0.js:10549:81)
at http://localhost:1234/js/angular-1.2.0.js:10635:26
at Scope.$eval (http://localhost:1234/js/angular-1.2.0.js:11528:28)
at Scope.$digest (http://localhost:1234/js/angular-1.2.0.js:11373:31)
at Scope.$delegate.__proto__.$digest (<anonymous>:844:31)
at Scope.$apply (http://localhost:1234/js/angular-1.2.0.js:11634:24)
at Scope.$delegate.__proto__.$apply (<anonymous>:855:30)
The service I am testing:
angular.module('services.field', [])
.factory('Field', ['$http', '$cacheFactory', function ($http, $cacheFactory) {
var fieldListCache = $cacheFactory('fieldList');
var Field = function (data) {
angular.extend(this, data);
};
// add static method to retrieve all fields
Field.query = function () {
return $http.get('api/ParamSetting', {cache:fieldListCache}).then(function (response) {
var fields = [];
angular.forEach(response.data, function (data) {
fields.push(new Field(data));
});
return fields;
});
};
// add static method to retrieve Field by id
Field.get = function (id) {
return $http.get('api/ParamSetting/' + id).then(function (response) {
return new Field(response.data);
});
};
// add static method to save Field
Field.prototype.save = function () {
fieldListCache.removeAll();
var field = this;
return $http.post('api/ParamSetting', field ).then(function (response) {
field.Id = response.data.d;
return field;
});
};
return Field;
}]);
The unit test that is failing:
'use strict';
describe('Field service', function() {
var Field, $httpBackend;
// load the service module
beforeEach(module('services.field'));
// instantiate service
beforeEach(inject(function(_Field_, _$httpBackend_) {
Field = _Field_;
$httpBackend = _$httpBackend_;
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe("save()", function() {
it('should clear field array from cache', function () {
var firstMockData = [{ Alias: 'Alias 1' }, { Alias: 'Alias 2' }];
var secondMockData = [{ Alias: 'Alias 3' }];
var newField = new Field({});
var counter = 0;
$httpBackend.when('GET', 'api/ParamSetting').respond(function () {
// return firstMockData on first request and secondMockdata on subsequent requests
if (counter === 0) {
counter++;
return [200, firstMockData, {}];
} else {
return [200, secondMockData, {}];
}
});
$httpBackend.when('POST', 'api/ParamSetting').respond({});
// query fields
Field.query();
// save new field
newField.save();
// query fields again
Field.query().then(function (data) {
expect(data.length).toBe(secondMockData.length);
expect(data[0].Alias).toBe(secondMockData[0].Alias);
});
$httpBackend.flush();
});
});
});
The answer is that I am erroneously expecting asynchronyous requests to return responses in a particular order, and that my requests are cached until I call $httpBackend.flush() which would lead to .query() only being called once. To make it work, one can make the calls synchronous by adding another flush after the first query() call: http://plnkr.co/edit/MzuplQnkQunDyvy6vCvy?p=preview
The following code will allow you to mock out the $cacheFactory in your unit tests. The $provide service will allow the service dependency injection to use your $cacheFactory instead of the default $cacheFactory.
var cache, $cacheFactory; //used in your its
beforeEach(function(){
module(function ($provide) {
$cacheFactory = function(){};
$cacheFactory.get = function(){};
cache = {
removeAll: function (){}
};
spyOn(cache, 'removeAll');
spyOn($cacheFactory, 'get').and.returnValue(cache);
$provide.value('$cacheFactory', $cacheFactory);
});
});
describe('yourFunction', function(){
it('calls cache.remove()', function(){
yourService.yourFunction();
expect(cache.remove).toHaveBeenCalled();
});
});