AngularJS JasmineJS decorated $log object not injected correctly - angularjs

I am new to AngularJS and JasmineJS.
I have decorated the Angular $log service to do logging over http. I verified that it works fine in the program itself. However, when running the Jasmine unit tests, the $log calls create errors. I get messages such as the following:
TypeError: TypeError: $log.warn.logs is undefined in
http...[snip]...angular-mocks.js (line 316)
The messages follow this pattern for all the different $log functions ($log.error.logs, $log.info.logs, etc.).
My current injector looks like this:
var $httpBackend, log;
beforeEach(inject(function ($injector, $log) {
log = $log;
//lots of mock http service things here
}));
I have also tried this:
beforeEach(inject(['$log', function (log) {
$log = log;
}]));
And this:
beforeEach(inject(function ($injector) {
log = $injector.get('$log');
// mock http things here
}));
A failing test looks like this:
it('should log error', function () {
$httpBackend.expectPOST('the url for logging');
var controller = createController();
log.warn('Test warning');
$httpBackend.flush();
});
All calls to $log in the code being tested result in the same error. When I commented them out and removed the failing test above, all unit tests passed.
How should I set up the unit tests correctly?

The answer is that there is something wrong with angular-mocks.js and it is not instantiating the log arrays correctly. I downloaded the latest non-beta copy, and it's still doing the same thing. The instantiation of arrays happens in their code using a call to $log.reset(), but somehow, that part of their code is not being reached. Therefore, I fixed my problem by manually calling $log.reset():
//This works
var $log;
beforeEach(inject(function ($injector) {
$log = $injector.get('$log');
$log.reset();
//continue other stuff
}));
//This also works
var log;
beforeEach(inject(function ($injector, $log) {
log = $log;
log.reset();
//continue other stuff
}));

Related

Testing $log.error in provider with Angular and Jasmine

I have a module that sends an error via $log.error(). This is working and I'm trying to finish the tests for it. My problem is that I'm unable to validate that it logs an error during my spec.
Plunkr
code:
var app = angular.module('plunker', []);
app.provider('MordorProvider', function($logProvider, $injector) {
// we gotta do this cuz we're a provider and don't have access to $log yet
var $log = angular.injector(['ng']).get('$log');
this.makeMeAnErrorWorthyOfMordor = function () {
$log.error('You shall not pass!');
}
this.$get = function () {
return this;
};
});
test:
describe('Gandalfs last stand', function() {
var MordorProvider,
$log;
beforeEach(module('plunker'));
beforeEach(inject(function(_MordorProvider_, _$log_) {
MordorProvider = _MordorProvider_;
$log = _$log_;
}));
it('should shout out!', function() {
MordorProvider.makeMeAnErrorWorthyOfMordor();
expect($log.error.logs).toContain('You shall not pass!');
});
});
I understand that my spec is using ngMock so that I have access to $log.error.logs, but if it doesn't have access to the same $log data, I'm confused on how to test this. If you open the console on that plunkr, you can see that the error is being emitted.
angular.injector creates a new injector instance and thus, new $log service instance. It isn't supposed to be used inside of Angular app in production, there are too few valid use cases for it.
This
app.provider('MordorProvider', function($logProvider, $injector) {
// we gotta do this cuz we're a provider and don't have access to $log yet
var $log = angular.injector(['ng']).get('$log');
...
});
can't be considered a workaround to use $log in provider. There's simple a reason why service instances aren't available for injection in service providers - they just aren't instantiated yet.
In this case $log variable refers to $log instance which can't be injected anywhere else and can't be tested. This is why the spec fails on $log.error.logs.
There's nothing in this service that would require provider instead of factory. When $log is injected and used in normal way, it can be safely tested with
beforeEach(module('plunker', {
$log: {
error: jasmine.createSpy()
}
}));
...
expect($log.error).toHaveBeenCalledWith('You shall not pass!');
Since the usage of $window service is the only thing that makes $log unsuitable for injection into config and provider, it can be cloned and modified to evade this limitation:
angular.module('uniformLog', []).config(function ($provide, $injector, $logProvider) {
var uniformLog = $injector.invoke($logProvider.$get, null, { $window: window });
$provide.constant('uniformLog', uniformLog);
// or $provide.constant('$log', ...) to override it entirely
});
angular.module('app', ['uniformLog']).config(function ($logProvider, uniformLog) {
$logProvider.debugEnabled(false);
uniformLog.debug('...');
});
You should just mock the $log.error function and verify that it was called
beforeEach(function() {
spyOn($log, 'error');
}
it('should shout out!', function() {
MordorProvider.makeMeAnErrorWorthyOfMordor();
expect($log.error).toHaveBeenCalledWith('You shall not pass!');
});

$httpBackend.flush() method throws Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting

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.

How to check the http request in unit testing?

I am trying to write an unit test for my app. It contains http request call in my case.
Test file
'use strict';
describe('Controller: testCtrl', function () {
// load the controller's module
beforeEach(module('myApp'));
var testCtrl, scope, $httpBackend;
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, $cookies) {
scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
testCtrl = _$controller_('testCtrl', {
$scope: scope,
});
}));
it("should return product data", function() {
$httpBackend.whenGET('/api/store/' + productID + '/products').respond([{
//not sure what I should do next...
}])
})
controller file
$http.get(('/api/store/' + productID + '/products').success(
function(data) {
//the data could contain 10 objects with 10+ property within each object.
}
);
Since the http request return a very complex object, I am not sure how to write my test. Can anyone help me about it? Thanks a lot!
You assume that your API works correctly, and what you're trying to actually test is:
does your app respond to the URL it should?
does it do any processing of th data it should?
So return a mock object in your whenGET, with enough detail for any data processing code.
As far as the test goes, you will have to return a mock object response. That being said, you do not need to pollute your test case with your 1000 line mock JSON. Simply save it in a separate file and use karma-ng-json2js-preprocessor to return it from the whenGET.

AngularJS "No pending request to flush" while unit testing a controller with a $resource

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.

Karma ignores angular http get request

I am using Karma to test a function within an angular directive. The function executes fine but the http.get request in it seems to be ignored where as others throw an unexpected get error.
beforeEach(inject(function($injector, $compile, $rootScope){
$gCompile = $compile;
$gScope = $rootScope;
$httpBackend = $injector.get('$httpBackend');
$gHttp = $httpBackend;
}));
it("Should login to the UI", function() {
//Compile directive
var element = angular.element('<my-app></my-app>');
$gCompile(element)($gScope);
//Reference it's local scope
var dirScope = element.scope();
$gScope.$digest();
dirScope.login();
});
The get request is in the login function.
It might help if you can show us what the login() function looks like, but for now it looks like you're not using $httpBackend correctly.
Take a look at http://docs.angularjs.org/api/ngMock/service/$httpBackend.
You should set up an expectation on $httpBackend, then call your login() function, then call flush() on $httpBackend.

Resources