Unit testing two dependent services and controller in AngularJS - angularjs

I have a demo application where I have a controller which has a factory as dependency and factory itself depends on another service. My code is as follows:
var app = angular.module('sampleApp', ['ui.router']);
app.service("someServ", function(){
this.sendMsg = function(name){
return "Hello " + name;
}
})
app.factory("appFactory", function ($http, someServ) {
function getData(url) {
return $http.get(url);
}
function foo(){
var text = someServ.sendMsg("Monotype");
alert(text);
}
return {
getData : getData,
foo : foo
}
})
var productsController = function ($scope, $http, appFactory) {
var pct = this;
pct.url = "http://mysafeinfo.com/api/data?list=englishmonarchs&format=json";
var jsonDataPromise = appFactory.getData(pct.url);
jsonDataPromise
.then(function (response) {
pct.jsonData = response.data;
}, function (err) {
console.log("Error is: " + error);
});
pct.profun = function(){
appFactory.foo();
}
};
app.controller("productsController", productsController);
productsController.$inject = ['$scope', '$http', 'appFactory'];
I have to test with karma using Jasmine 2.4 as testing framework. I have tried many online tutorials but getting totally confused as everyone tries to do something different. Some use $provide to mock the service, some simply inject the actual service/factory and use a reference, some do not give any example of passing arguments to services.
Can someone please tell me how to do unit testing in simple terms. I have already tried doing something like this:
describe('unit testing of service and controller', function(){
beforeEach(module('sampleApp'));
var prodCtrl, $prodScope, mockfactory, mockservice;
beforeEach(function(){
mockservice = {
sendMsg : function(name){
return name;
}
}
module(function($provide){
$provide.value("someServ", mockservice);
});
inject(function($rootScope, $controller, $http, $q, appFactory){
appFactory = appFactory;
spyOn(appFactory, 'getData');
spyOn(appFactory, 'foo');
$prodScope = $rootScope.$new();
prodCtrl = $controller('productsController', {
$scope: $prodScope, $http: $http, appFactory:appFactory
});
});
});
it('appFactory has method getData and foo', function(){
appFactory.getData();
appFactory.foo();
expect(appFactory.getData).toHaveBeenCalled();
expect(appFactory.foo).toHaveBeenCalled();
})
it('productsController gets a promise back from getData', function(){
var url = "sample url";
var myPromise = prodCtrl.getData(url);
myPromise.then(function(){console.log("Promise returned");})
})
it('foo calls service method sendMsg', function(){
prodCtrl.profun();
expect(mockservice.sendMsg).toHaveBeenCalled();
})
});

I was finally able to solve this issue. My code looks like this:
var app = angular.module('sampleApp', []);
app.service("someServ", function(){
this.sendMsg = function(name){
return "Hello " + name;
}
})
app.factory("appFactory", function ($q, someServ) {
function getData() {
var defer = $q.defer();
defer.resolve("Success message");
return defer.promise;
}
function foo(){
var text = someServ.sendMsg("Monotype");
alert(text);
}
return {
getData : getData,
foo : foo
}
})
app.controller("mainController", ['$scope', '$http','appFactory', function($scope, $http, appFactory){
var mct = this;
mct.printData = function(){
var myPromise = appFactory.getData();
myPromise
.then(function(data){
alert("Promise returned successfully. Data : " + data);
}, function(error){
alert("Something went wrong.... Error: " + error);
})
}
mct.showMsg = function(){
appFactory.foo();
}
}]);
The test case looked like this:
describe('unit testing', function(){
var jsonData = {
name: "Aosis",
id: 12345
}
beforeEach(module('sampleApp'));
beforeEach(module(function($provide){
$provide.service("someServ", function(){
//this.sendMsg = function(param){}
this.sendMsg = jasmine.createSpy('sendMsg').and.callFake(function(param){})
});
$provide.factory("appFactory", function(someServ, $q){
function getData(){
var defer = $q.defer();
defer.resolve("Success message");
return defer.promise;
}
function foo(){
var facParam = "some text";
someServ.sendMsg(facParam);
}
return {
getData : getData,
foo : foo
}
});
}));
var $scope, mainController, appFactoryMock, someServMock;
beforeEach(inject(function($rootScope, $controller, $http, $q, appFactory, someServ){
appFactoryMock = appFactory;
someServMock = someServ;
$scope = $rootScope.$new();
mainController = $controller("mainController", {
$scope : $scope,
$http : $http,
appFactory : appFactoryMock
});
}));
// Tests go here....
});
Here, I have mocked service method as jasmine spy and specified the function that should get executed usingand.callFake(function(){.....}). A fake factory has been created and its methods have been spied upon. I tried to create fake factory similar to service using jasmine.createSpy but return {
getData : getData,
foo : foo
} was giving error. Hence, I did that.
Anyone, else if has better solution or some other explanation, please share.

Related

Unit Testing Promises in AngularJS Error: Expected a spy, but got getCtrl({ })

I have service created which returns product details via httppost request
I have controller through which i call service __getProductService.getproductDetailsPull().then(function(response){__
and i get the data in controller
I wrote a test case for this in jasmine-karma by injecting spy
__spyOn(getProduct, 'getproductDetailsPull').and.returnValue(deferred.promise);__
**But i got errors for promises **
Error 1
Expected a spy, but got deleteCtrl({ }).
Error 2
.then is not a function
Service Code
var myapp = angular.module('abcservice');
myapp.service('getProductService',function($http,$q){
var productDetails = [];
var productResponse = null;
this.setproduct= function() {
var obj = {
adminId : 15,
productOrderID: 174824929577
};
if (this.productResponse == null) {
this.productResponse = $http.post('someurl',obj).success(function(data, status, headers,config) {
this.productResponse = mapJson(data);
}).error(function(data, status, headers,config)
{
console.log("error while fetching data from spring controller:" +error);
});
}
return this.productResponse;
};
this.getproductDetailsPull = function(productResponse) {
return this.productResponse;
};
}
Controller Code
angular
.module('getCtrl', []);
getCtrl.$inject = ['$scope', '$http', '$rootScope', 'getProductService'];
function getCtrl($scope, $http, $rootScope, getProductService) {
getProductService.getproductDetailsPull().then(function(response){
$scope.displayData = response.data.productorder;
$scope.lineItemData = response.data.OrderItem;
}
}
Jasmine Test Case
describe('getCtrl Test', function() {
var $scope = null;
var $getProduct = null;
var $rootScope = null;
var deferred,$q;
beforeEach(module('abcservice','getCtrl'));
beforeEach(inject(function (_$controller_,$rootScope,getProduct,_$q_) {
$controller = _$controller_;
$scope = $rootScope.$new();
$q = _$q_;;
deferred = _$q_.defer();
spyOn(getProduct, 'getproductDetailsPull').and.returnValue(deferred.promise);
controller = $controller('getCtrl', { $scope: $scope,$rootScope: $rootScope,getProduct:getProduct });
}));
it('Exists controller, function() {
expect(controller).toHaveBeenCalled();
});
});
You have a typo, getProduct is not the name of your service. You need to inject the service like:
beforeEach(inject(function (_$controller_,$rootScope,getProductService
The spy should be int he format spyOn(object, "methodName"), so in your case:
spyOn(getProductService, 'getproductDetailsPull')
Consider doing this for your promise:
spyOn(getProductService, 'getproductDetailsPull').and.returnValue($q.when())
Your test case is a little weird I assume your just doing that to get things working but you probably want something like:
it('Product is fetched, function() {
scope.$digest(); // if your using $q you need this (maybe move it to before call)
expect(getProductService.getproductDetailsPull).toHaveBeenCalled();
});

How to test Controller that calls a service where service uses $http

I am trying to test a controller. The controller uses a service which is using $http to get the data from a json file (This json file is just a mock up of response returned from server)
My problem is that when I am testing the controller, it creates the controller object and even calls the service. But it doesnt call the $http mocked response. I not sure where I am going wrong. I tried looking at few examples but all of them are using $q.
My service looks like this:
(function(){
angular.module('mymodule')
.factory('MyService', MyService);
MyService.$inject = ['$http'];
function MyService($http) {
var service = {
retrieveData : retrieveData
};
return service;
function retrieveData(containerLabel){
var myGrossData = [];
var isMatchFound = false;
var myindex = containerLabel.slice(-4);
return $http.get('app/myGrossData.json').then(function(response) {
console.log('inside http retrieveData: ');
myGrossData = response.data;
var myindexExists = false;
var mydataObject = [];
var defaultdata = [];
angular.forEach(myGrossData, function (myGrossData) {
if (myindex === myGrossData.myindex) {
mydataObject = myGrossData;
isMatchFound = true;
}
if(!isMatchFound && myGrossData.myindex === '2006')
{
mydataObject = myGrossData;
}
if(myGrossData.myindex === '2006'){
defaultdata = myGrossData;
}
});
if (isMatchFound && response.status === 200)
{
return mydataObject;
}
else if(!isMatchFound && (response.status === 200 || response.status === 201)){
return defaultdata;
}
else //all other responses for success block
{
return 'Incorrect Response status: '+response.status;
}
},
function(error){
return 'Error Response: '+error.status;
}
);
}
};
})();
The controller calling it is :
(function () {
'use strict';
angular
.module('mymodule', [])
.controller('MyCtrl', MyCtrl);
MyCtrl.$inject = ['$scope', 'MyService'];
function MyCtrl($scope, MyService) {
var vm = this;
vm.datafromsomewhere = datafromsomewhere;
vm.displayData = [];
vm.disableBarCode = false;
vm.childCount = 0;
vm.headertext="Master Container Builder";
init();
function init() {
console.log('MyCtrl has been initialized!');
console.log(vm.headertext);
}
function myfunctionCalledByUI(input) {
processData(input);
}
function processData(containerLabel){
MyService.retrieveMasterContainer(containerLabel).then(function(data){
vm.displayData = data;
});
vm.disableBarCode = true;
vm.childCount = (vm.displayData.childData === undefined) ? 0: vm.displayData.childData.length;
vm.headertext="Myindex "+vm.displayData.myindex;
if ( vm.displayData.masterDataId.match(/[a-z]/i)) {
// Validation passed
vm.displayData.masterDataId ="No Shipping Label Assigned";
}
else
console.log('else: '+vm.displayData.masterDataId);
console.log('length of childData: '+vm.childCount);
}
}
})();
and finally my spec looks like this:
var expect = chai.expect;
describe('Test Controller', function () {
var rootScope, compile; MyService = {};
var $scope, $controller;
beforeEach(module('ui.router'));
beforeEach(function() {
module('mymodule');
inject(function ($rootScope, _$compile_,_$controller_) {
rootScope = $rootScope;
compile = _$compile_;
$scope = $rootScope.$new();
MyService = jasmine.createSpyObj('MyService', [
'retrieveData'
]);
$controller = _$controller_('MyCtrl', {
$scope: $scope
});
});
});
it('controller should be initialized and data should also be initialized', function() {
expect($controller).to.not.be.undefined;
expect($controller).to.not.be.null;
expect($controller.disableBarCode).to.equal(false);
expect($controller.childCount).to.equal(0);
expect($controller.headertext).to.equal("Master Container Builder");
});
it(' should process data when containerLabel is called into myfunction', function() {
$controller.handKeyed('12001');
expect(MyService.retrieveData).to.have.been.called;
expect($controller.processData).to.have.been.called;
expect($controller.disableBarCode).to.equal(true);
expect($controller.childCount).to.equal(0);
expect($controller.headertext).to.equal("Master Container Builder");
});
});
I am using following techstack if it helps:
angular 1.5
Ionic
Karma-jasmine
The code works when I run it. My issue is that when i run the test it doesnt populate the data in my vm.displayData variable. how do I make it get some data into the service. I added in some log statements and it skips it completely.
After all the test run including unrelated tests to this one, then I see the log statements from MyService. I am not sure how to approach this.
I think what you are looking for is the $httpBackend service. It will mock the request indicating the result. So, when your service hit the url, it will return what you passed to the $httpBackend configuration.
A simple example would be:
it('should list newest by category', function(){
$httpBackend
.expectGET(url)
.respond(techPosts /*YOUR MOCKED DATA*/);
$stateParams.category = 'tech';
var controller = $controller('HomeCtrl', { PostsResource: PostsResource, $stateParams: $stateParams });
controller.listNewestPosts();
$httpBackend.flush();
expect(controller.posts).toEqual(techPosts.posts);
});

Provider mock error: Expected a spy, but got Function

I'm very new to jasmine and I got some issue trying to mock a provider.
I have a provider that looks like :
angular.module('myApp')
.factory('MyService', function ($resource) {
return {
actionResource : function(projectId, actionId){
var url = 'blablabla';
if(actionId){
url += "/"+actionId;
}
return $resource(url, {}, {
'create': {
method: 'POST'
}
});
},
};
});
I have a controller using this factory
angular.module('myApp')
.controller('myCtrl', function ($scope, MyService, $state) {
$scope.addAction = function(){
MyService.actionResource($scope.projectId).create($scope.action,
function(){
$state.go('somewhere', {});
});
};
});
and I'd really like to test this controller, at the moment I'm doing something like :
'use strict';
describe('[test] [controller] - myCtrl', function() {
beforeEach(angular.mock.module('myApp'));
//mock creation
beforeEach(
module(function($provide) {
$provide.factory('MyService', function() {
var actionResource = function(projectId, actionId){
var create = function(){return {}};
return {create:create};
}
return {actionResource:actionResource}
}
)})
);
var $httpBackend, $rootScope, myController, mockMyService;
var scope = {};
beforeEach(angular.mock.inject(function($injector, $rootScope, MyService) {
var $controller = $injector.get('$controller');
scope=$rootScope.$new();
mockMyService = MyService;
spyOn(mockPilotageService, 'actionResource').and.callThrough();
spyOn(mockPilotageService.actionResource(), 'create');
createController = function() {
return $controller("myCtrl", {$scope:scope, myService:mockMyService});
};
}));
it('myCtrl - addAction() calls actionRessource.create() method', function() {
createController();
scope.addAction();
expect(mockMyService).toBeDefined();
expect(mockMyService.actionResource).toHaveBeenCalled();
expect(mockMyService.actionResource().create).toHaveBeenCalled();
});
});
and I'm getting this error :
Error: Expected a spy, but got Function.
at /Users/*****/myController.spec.js:86
So it's able to spy on mockMyService.actionResource but not on mockMyService.actionResource().create. I can't understand why
any help would be more than welcome
thanks

How to avoid errors with unit testing a controller?

I am trying to unit test a controller. This is my controller:
app.factory('myService', function ($q) {
var callMe = function (user) {
var pr = $q.defer();
pr.resolve('Hello ' + user);
return pr.promise;
//$timeout(function(){
// pr.resolve('Hello ' + user);
// return pr.promise;
//},4000);
}
return {callMe: callMe};
});
app.controller('myCtrl',function($scope,myService){
$scope.callService = function(){
$scope.callMeValue = myService.callMe('lo');
}
})
This is my test:
beforeEach(
inject(function (_$rootScope_, $controller, _myService_, _myServiceTimeout_,$q) {
myService = _myService_;
myServiceTimeout = _myServiceTimeout_;
$scope = _$rootScope_.$new();
ctrl = $controller('myCtrl', {
$scope: $scope,
someService: someServiceMock
});
someServiceMock.callMe.andReturn($q.when('Ted'));
}));
it('ctrl test', function () {
$scope.callService();
expect(myService.callMe).toHaveBeenCalled();
});
Here are the errors I am getting:
TypeError: someServiceMock.callMe.andReturn is not a function
and:
Error: Expected a spy, but got Function.
How can I fix this?
plunkr: http://plnkr.co/edit/EM1blTOlg5fw5wq6OFcr?p=preview
Your example contains several bugs.
If you use timeout in code, in test you must use $timeout.flush() (scope.$apply not enough)
$timeout is promise, you not need create own promise
$timeout is promise, you must return it
app.factory('myServiceTimeout', function ( $timeout) {
var callMe = function (user) {
return $timeout(function(){
return 'Hello ' + user;
},4000);
}
return {callMe: callMe};
});
it('test2',function(){
var result;
myServiceTimeout.callMe('Ruud').then(function(ret)
{
result = ret;
});
$timeout.flush()
expect(result).toBe('Hello Ruud');
});
whole exemple: http://plnkr.co/edit/cqzTYwfs94Xqyz5MTxeE?p=preview

AngularJS Factory: undefined is not a function

I wrote the following factory service in AngularJS, but when I try to call the factory service in my RootController, I got the "undefined is not a function" error in my console.
MyService.js:
(function() {
angular.module('serviceTestApp')
.factory('MyService', ['$resource', '$log', '$q', '$http', MyService]);
function MyService($log, $resource, $http, $q) {
var name = "Tom";
var getName = function() {
return name;
}; //getName
var changeName = function(newName) {
name = newName;
}; //changeName
var getIP = function() {
var deferredObj = $q.defer();
$resource('http://jsonip.com').query().$promise.then(function(result) {
deferredObj.resolve(result);
}, function(errorMsg) {
deferredObj.reject(errorMsg);
});
return deferredObj.promise;
}; //getIP
return {
getName: getName,
changeName: changeName,
getIP: getIP
};
}
}());
in my RootController, I try to call the services, and everything worked until I call the getIP() service - return a promise object. Does anyone see anything wrong?
RootController.js:
(function() {
angular.module('serviceTestApp')
.controller('RootCtrl', ['$http', '$log', '$scope', 'MyService', RootCtrl]);
function RootCtrl($log, $scope, $http, MyService) {
var vm = this;
vm.message = "hello world from RootController";
MyService.changeName("Henry Tudor");
vm.message = "my name is: " + MyService.getName();
MyService.getIP().query().then(function(data) {
$log.info('in the promise, ip is: ' + data.ip);
vm.message = vm.message + ', your IP is ' + data.ip;
}, function(error) {
vm.message = vm.message + ', error: ' + error;
});
}
}());
It may sound stupid, but I've located the cause for the error:
The API used in the $resource() returns a single JSON object:
{"ip":"2601:0:b840:8077:a97f:ee9c:f5b8:1643","about":"/about","Pro!":"http://getjsonip.com"}
however, the query() expects an array, not a JSON object.
After changing to another API that returns an array in JSON format, it works. Sigh, wasted my 2 hours.
thanks everyone
in order to use your service like that
MyService.getName()
that mean you have a service called MyService and returns a function called getName
but in your case it dosen't you have to change
your return to something like that
return {
getName: function() {return getName();},
changeName: function() {return changeName();},
getIP: function() {return getIP();}
};

Resources