I'm new to jasmine framework. I've gone through some tutorials and learned and started writing unit tests. 'facing one issue here is the description.
I have a controller where i can invoke a service call to get the data. See the code below.
$scope.getEmpInfo = function() {
EmpService.getInfo($scope.empid)
.then(function(data) {
$scope.empData = data;
$scope.populateEmpData($scope.empData);
}, function(reason) {
//do nothing
}
}
Now, i want to write a unit test for the above method. Im able to make a spy on serice using promise but i wasnt able to spy $scope.populateEmpData(). here is my test case.
describe('Emp data', function() {
var d, scope;
beforeEach(function() {
module("emp");
module("emo.info");
});
describe('empcontroller', function() {
beforeEach(inject(function($q,_EmpService_, $controller,$rootScope){
d = $q.defer();
empService = _EmpService_;
spyOn(empService,"getInfo").and.returnValue(d.promise);
scope = $rootScope.$new();
empCtrl = $controller("empController", {
$scope: scope,
});
}));
it('should get the Employee information ', function() {
scope.getEmpInfo();
spyOn(scope,'populateEmpData');
expect(EmpService.getInfo).toHaveBeenCalled();
//Here im getting the error.
expect(scope.populateEmpData).toHaveBeenCalled();
});
});
});
Please help resolve this issue. Thanks in advance.
It's because you are not resolving promise. You will have to make change in spyOn.
- spyOn(empService,"getInfo").and.callFake(function() {
return {
then : function(success, error) {
success();
}
} }
Now, it will go into the success callback and will try to call $scope.populateEmpData();
You're never resolving your promise. And you need to call $scope.$apply().
Why is this necessary? Because any promises made with the $q service
to be resolved/rejected are processed upon each run of angular’s
digest cycle. Conceptually, the call to .resolve changes the state of
the promise and adds it to a queue. Each time angular’s digest cycle
runs, any outstanding promises will be processed and removed from the
queue.
Unit Testing with $q Promises in AngularJS
Check it out above link it will help you.
Related
I am trying to test my controller using jasmine. Basically, when the controller is created it will call a service to make http request. I am using httpBackend to get the fake data. When I try to run the test I always get the error "No pending request to flush". If I remove the httpBackend.flush() then the test fails because controller.data.name is undefined. Can anyone know why it happens like that? Thanks.
The code for the module is here:
var myModule = angular.module('myModule', ['ngMockE2E']);
myModule.run(function($httpBackend){
$httpBackend.whenGET('/Person?content=Manager').respond(function (){
var response = {'name':'Bob','age':'43'}
return [200,response];
})
});
The code for the service:
myModule.factory('myService',function($http){
return {
getData: function(position){
return $http.get('/Person?content='+position);
}
}
});
The code for controller is:
myModule.controller('myController',function(xrefService){
var _this = this;
_this.data ={};
_this.getData = function(position){
myService.getData(position).then(function(response){
_this.data = response.data
});
}
_this.getData("Manager");
})
The code to test the controller is:
describe("Test Controller",function(){
var controller,httpBackend,createController;
beforeEach(module('myModule'));
beforeEach(inject(function($controller,$httpBackend){
createController = function(){
return $controller('myController');
}
httpBackend = $httpBackend;
}));
it("should return data",function(){
controller = createController();
httpBackend.flush();
expect(controller.data.name).toEqual("Bob");
});
})
The angular documentation says the following about $httpbackend for ngMockE2E:
Additionally, we don't want to manually have to flush mocked out
requests like we do during unit testing. For this reason the e2e
$httpBackend flushes mocked out requests automatically, closely
simulating the behavior of the XMLHttpRequest object.
So, short answer: it doesn't exist and you don't need it.
you are using $httpBackend.whenGET inside "The code for the module"
you should be using $httpBackend inside the test code as follows ...
it("should return data",function(){
$httpBackend.expectGET('/Person?content=Manager').respond(function (){
var response = {'name':'Bob','age':'43'}
return [200,response];
})
controller = createController();
httpBackend.flush();
expect(controller.data.name).toEqual("Bob");
});
also i would advise using expectGET instead of whenGET.
With whenGET you are saying if the request is made then response like so.
With expectGET you are saying ... a request will be made, when it is made respond like so, if the request is not made then fail the test.
PS if you put some console.log statements inside your controller code then you should see these log statements when you run your test suite. If not then you know your controller code is not even being hit.
also use ..
afterEach(function () {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
which will force test failure if expectations were not met.
I am trying to unit test my AngularJS application using Karma and Jasmine. I want to mock the $http service. For that, I am using the $httpBackend method. Below is my service that I want to test:
angular.module('MyModule').factory('MyService', function($http, $log, $parse, $q, $timeout, $filter, MyOtherService1, MyOtherService2){
var service = {};
service.getSomething = function(id){
return $http.get('/somePath/subpath/' + id);
}
});
My unit test for this service is:
describe("myTest", function(){
var myService, $httpBackend, scope, mockMyOtherService1, mockMyOtherService2;
var myResponse =
{
foo:'bar'
};
beforeEach(module("MyModule"));
beforeEach(inject(function(_MyService_, $injector){
$httpBackend = $injector.get('$httpBackend');
myService = _MyService_;
scope = $injector.get('$rootScope').$new();
mockMyOtherService1 = $injector.get('MyOtherService1');
mockMyOtherService2 = $injector.get('MyOtherService2');
}));
beforeEach(function(){
//To bypass dependent requests
$httpBackend.whenGET(/\.html$/).respond(200,'');
});
//If I uncomment the below afterEach block, the same error is shown at next line.
/*afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});*/
//This test passes successfully
it("should check if service is instantiated", function () {
expect(myService).toBeDefined();
});
//This test passes successfully
it("should expect dependencies to be instantiated", function(){
expect($httpBackend).toBeDefined();
});
//The problem is in this test
it("should get the getSomething with the provided ID", function() {
$httpBackend.whenGET('/somePath/subpath/my_123').respond(200,myResponse);
var deferredResponse = myService.getSomething('my_123');
//The error is shown in next line.
$httpBackend.flush();
//If I comment the $httpBackend.flush(), in the next line, the $$state in deferredResponse shows that the Object that I responded with is not set i.e. it does not matches the 'myResponse'.
expect(deferredResponse).toEqual(myResponse);
});
});
This is an emergency problem and I need help regarding the same as soon as possible. I will be very grateful for your answer.
The problem was I needed to inject $location in my spec files even though they are not injected in the services. After injection, all worked well! Hope this helps someone who gets stuck in the same situation.
You will get a promise from your service. So change your test code to:
//The problem is in this test
it("should get the getSomething with the provided ID", function (done) {
$httpBackend.expectGET('/somePath/subpath/my_123').respond(200,myResponse);
var deferredResponse = myService.getSomething('my_123');
deferredResponse.then(function (value) {
expect(value.data).toEqual(myResponse);
}).finally(done);
$httpBackend.flush();
});
I've recently had this problem when updating a project from Angular 1.2 to 1.4. The test code looked something like:
it('should call /something', function(){
httpBackend.expectGET('/something').respond(200);
scope.doSomething();
httpBackend.flush();
});
The error was the infdig past 10 iterations. It was caused by invoking the .flush() method. I figured out this is seemingly because there were no pending promises created within doSomething().
Once I added a promise somewhere within doSomething() or inner methods, the infdig problem went away.
I suspect - and this is 100% speculation so don't let it influence your development - this is because httpBackend does some trickery to wait for promises, which maybe involves digesting repeatedly until there's a change. Since there's no promises, there's no changes - infinite digest.
It seems that promises do not resolve in Angular/Jasmine tests unless you force a $scope.$digest(). This is silly IMO but fine, I have that working where applicable (controllers).
The situation I'm in now is I have a service which could care less about any scopes in the application, all it does it return some data from the server but the promise doesn't seem to be resolving.
app.service('myService', function($q) {
return {
getSomething: function() {
var deferred = $q.defer();
deferred.resolve('test');
return deferred.promise;
}
}
});
describe('Method: getSomething', function() {
// In this case the expect()s are never executed
it('should get something', function(done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
expect(1).toEqual(2);
});
done();
});
// This throws an error because done() is never called.
// Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
it('should get something', function(done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
expect(1).toEqual(2);
done();
});
});
});
What is the correct way to test this functionality?
Edit: Solution for reference. Apparently you are forced to inject and digest the $rootScope even if the service is not using it.
it('should get something', function($rootScope, done) {
var promise = myService.getSomething();
promise.then(function(resp) {
expect(resp).toBe('test');
});
$rootScope.$digest();
done();
});
You need to inject $rootScope in your test and trigger $digest on it.
there is always the $rootScope, use it
inject(function($rootScope){
myRootScope=$rootScope;
})
....
myRootScope.$digest();
So I have be struggling with this all afternoon. After reading this post, I too felt that there was something off with the answer;it turns out there is. None of the above answers give a clear explanation as to where and why to use $rootScope.$digest. So, here is what I came up with.
First off why? You need to use $rootScope.$digest whenever you are responding from a non-angular event or callback. This would include pure DOM events, jQuery events, and other 3rd party Promise libraries other than $q which is part of angular.
Secondly where? In your code, NOT your test. There is no need to inject $rootScope into your test, it is only needed in your actual angular service. That is where all of the above fail to make clear what the answer is, they show $rootScope.$digest as being called from the test.
I hope this helps the next person that comes a long that has is same issue.
Update
I deleted this post yesterday when it got voted down. Today I continued to have this problem trying to use the answers, graciously provided above. So, I standby my answer at the cost of reputation points, and as such , I am undeleting it.
This is what you need in event handlers that are non-angular, and you are using $q and trying to test with Jasmine.
something.on('ready', function(err) {
$rootScope.$apply(function(){deferred.resolve()});
});
Note that it may need to be wrapped in a $timeout in some case.
something.on('ready', function(err) {
$timeout(function(){
$rootScope.$apply(function(){deferred.resolve()});
});
});
One more note. In the original problem examples you are calling done at the wrong time. You need to call done inside of the then method (or the catch or finally), of the promise, after is resolves. You are calling it before the promise resolves, which is causing the it clause to terminate.
From the angular documentation.
https://docs.angularjs.org/api/ng/service/$q
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);
}));
I am writing unit tests for a controller. This controller has a $resource service injected :
function controller($scope, Service) {
Service.get(function(result){
// do stuff with the result, not relevant here
}
}
The service is defined like this :
angular.module('so').factory('Service', ['$resource', service]);
function service($resource) {
return $resource('/url', null, {
get: { method: 'POST', params: {}, isArray: false}
});
}
My Jasmine unit test is the following :
describe("Controller", function(){
var $httpBackend;
beforeEach(function() {
module('so');
inject(function( _$httpBackend_) {
$httpBackend = _$httpBackend_;
});
});
it('should have done stuff irrelevant to the question', function() {
var $injector = angular.injector('so'),
$scope = $injector.get('$rootScope'),
$httpBackend
.whenPOST('/url')
.respond ([]);
// controller needs to be defined here and not in the beforeEach as there
// are more parameters passed to it, depending on the test
var controller = $injector.get('$controller')('controller', { "$scope": $scope });
$httpBackend.flush();
// then here the actual test resolution, also irrelevant
});
});
I get an error when running the test :
Error: No pending request to flush ! in file:///path/to/angular-mock.js (line 1453)
I added a console.log() in the callback from Service.get() and indeed, it is not called (everything outside of the callback is of course called). Also tried to add a scope digest if not phased after controller creation in the unit test, as I saw suggested in an other question, with no luck.
I know that I can mock that in some other ways, but using $httpBackend seems the perfect solution for the test : mocking the webserver and the data received.
I'm using AngularJS 1.2.16 (can't upgrade to 1.3.*, IE 8 compatibility required). I first used 1.2.13 and updated to check if it would solve the issue, without any luck.
That was an injection issue that was solved by changing the test from
it('should have done stuff irrelevant to the question', function() {
var $injector = angular.injector('so'),
$scope = $injector.get('$rootScope'),
$httpBackend
.whenPOST('/url')
.respond ([]);
// controller needs to be defined here and not in the beforeEach as there
// are more parameters passed to it, depending on the test
var controller = $injector.get('$controller')('controller', { "$scope": $scope });
$httpBackend.flush();
// then here the actual test resolution, also irrelevant
});
To:
it('should have done stuff irrelevant to the question', inject(function(Service) {
// edited lines because they did not change
var controller = $injector.get('$controller')('controller', { "$scope": $scope, "Service": Service });
// edited lines because they did not change
}));
So basicaly, adding the inject() in the test function and passing the service to the controller "manually".
I found the issue, that's great, but I don't really understand why it doesn't work. Also, I tried this right after finding the solution :
it('should have done stuff irrelevant to the question', inject(function() {
// edited lines because they did not change
var Service = $injector.get('Service'),
var controller = $injector.get('$controller')('controller', { "$scope": $scope, "Service": Service });
// edited lines because they did not change
}));
but this fail again, with the same "no pending request" error. I'm guessing that's some sort of racing issue, where my service can't get the proper $httpBackend to be injected when it's created afterwards, but I don't really understand why this occurs. If anybody can enlighten me... I'll be grateful.
See this plunkr for a live example: http://plnkr.co/edit/djQPW7g4HIuxDIm4K8RC
In the code below, the line var promise = serviceThatReturnsPromise(); is run during module configuration time, but I want to mock out the promise that is returned by the service.
Ideally I'd use the $q service to create the mock promise, but I can't do that because serviceThatReturnsPromise() is executed during module configuration time, before I can get access to $q. What's the best way to resolve this chicken and egg problem?
var app = angular.module('plunker', []);
app.factory('serviceUnderTest', function (serviceThatReturnsPromise) {
// We mock out serviceThatReturnsPromise in the test
var promise = serviceThatReturnsPromise();
return function() {
return 4;
};
});
describe('Mocking a promise', function() {
var deferredForMock, service;
beforeEach(module('plunker'));
beforeEach(module(function($provide) {
$provide.factory('serviceThatReturnsPromise', function() {
return function() {
// deferredForMock will be undefined because this is called
// when `serviceUnderTest` is $invoked (i.e. at module configuration),
// but we don't define deferredForMock until the inject() below because
// we need the $q service to create it. How to solve this chicken and
// egg problem?
return deferredForMock.promise;
}
});
}));
beforeEach(inject(function($q, serviceUnderTest) {
service = serviceUnderTest;
deferredForMock = $q.defer();
}));
it('This test won\'t even run', function() {
// we won't even get here because the serviceUnderTest
// service will fail during module configuration
expect(service()).toBe(4);
});
});
I'm not sure I like the solution much, but here it is:
http://plnkr.co/edit/uBwsJxJRjS1qqsKIx5j7?p=preview
You need to ensure that you don't instantiate "serviceUnderTest" until after you've set-up everything. Therefore, I've split the second beforeEach into two separate pieces: the first instantiates and uses $q, the second instantiates and uses serviceUnderTest.
I've also had to include the $rootScope, because Angular's promises are designed to work within a $apply() method.
Hope that helps.