Why is $log always silenced by angular-mocks? - angularjs

$log service is recommended over console.log for AngularJS apps. One common use case for such logging is seeing debug print when running tests. The problem is, angular-mocks silences $log by default replacing it by a mock. Well, I do sometimes need to test my debug print, but I just need to see it much more often. The problem is, the default behavior insists on using dummy logging and I don't even see a proper way to revert back to real $log. I've made a jsfiddle example to illustrate it, try running it while looking at devtools console http://jsfiddle.net/ivan4th/EnvL9/
var myApp = angular.module('myApp', []);
describe('myApp', function () {
var element, rootScope;
beforeEach(module('myApp'));
it('does something', inject(function ($log) {
$log.log("this message gets eaten by angular-mocks");
console.log("this message is visible though");
}));
});
The first message is skipped, while second is shown as expected.
Why such strange behavior is used and is there any way to fix it besides not using $log?

The mock $log service is designed to allow you to easily test and assert whether messages are logged or not, and ironically, to prevent log noise to the console when running tests.
If you don not like this behavior you can easily change it by doing:
$log.log = function(message){ console.log(message);};
Alternative you could output all previously logged messages to the console by doing:
console.log($log.log.logs)
You could add either method into a global before or after in your jasmine.

Based on an idea from dtabuenc's answer, I've implemented workaround that
dumps log outputs for Jasmine tests that fail, in the form of global
afterEach():
afterEach(inject(function ($log) {
// dump log output in case of test failure
if (this.results().failedCount) {
var out = [];
angular.forEach(["log", "info", "warn", "error", "debug"], function (logLevel) {
var logs = $log[logLevel].logs;
if (!logs.length)
return;
out.push(["*** " + logLevel + " ***"]);
out.push.apply(out, logs);
out.push(["*** /" + logLevel + " ***"]);
});
if (out.length) {
console.log("*** logs for: " + this.description + " ***");
angular.forEach(out, function (items) { console.log.apply(console, items); });
}
}
$log.reset();
}));
Unfortunately without replacing angular-mocks' $log implementation
the order of messages with different log levels is lost, but I guess
I'll be able to live with that. Silencing all log messages including
Angular's own errors and warnings during tests still sound like bad
design choice though.

Related

Unable to test a rejected promise after migrating to Angular 1.6.3

I have recently updated my application from Angular 1.5 to 1.6.3 and started getting Jasmine unit test failures (with PhantomJS) around promise based code I have written:
Possibly unhandled rejection: undefined thrown
Reading around I see that the accepted solution is to chain .then() with .catch() blocks to handle the rejections gracefully.
I have done this for one of my source files that I am testing to prove this gets past the error which it does.
However, it has now uncovered a further issue where an expectation I am testing when a promise rejection is called in my code is no longer passing.
This is the function I am trying to test (after adding the required catch blocks)
public deleteSomething = (thing) => {
return this.UserMessages.buildConfirmDialog().then(() => {
this.someService.remove(thing)
.then(() => {
this.UserMessages.showToast('Something deleted');
})
.catch((error) => {
//handle error
});
})
.catch((error) => {
//handle error
});
}
And here is the test:
var thing = {foo: 'bar'},
deferredRemove,
deferredConfirm,
//Mock service below injected into controller later on before test are run
UserMessages = {
buildConfirmDialog: jasmine.createSpy('buildConfirmDialog').and.callFake(function() {
deferredConfirm = $q.defer();
return deferredConfirm.promise.catch(angular.noop);
})
};
//Inject and controller setup here...
describe('When deleting something', function() {
beforeEach(function() {
deferredRemove = $q.defer();
spyOn(someService, 'remove').and.returnValue(deferredRemove.promise.catch(angular.noop));
});
describe('and the user confirms the deletion', function() {
beforeEach(function() {
ctrl.deleteSomething(thing);
deferredConfirm.resolve();
deferredRemove.resolve();
$rootScope.$apply();
});
it('should call remove on someService', function() {
console.log('someService.remove.calls = ' + someService.remove.calls.count());
expect(someService.remove).toHaveBeenCalled();
});
});
describe('and the user cancels the deletion', function() {
beforeEach(function() {
someService.remove.calls.reset();
vm.deleteSomething(thing);
deferredConfirm.reject({});
$rootScope.$apply();
});
it('should not call remove on someService', function() {
console.log('someService.remove.calls = ' + someService.remove.calls.count());
expect(someService.remove.calls.count()).toEqual(0);
});
});
});
I didnt have the .catch(angular.noop) parts in prior to upgrading to 1.6.3 and came across some posts suggesting to do this in order to make the tests happy, which certainly helped for me in getting past the unhandled rejection error in my test run.
The problem I am now facing is that for the reject test spec, there should be no call made to a remove function in my service, and so the number of calls should be zero, but it keeps coming out as 1. I added the line to reset the calls in my test to be sure it wasnt the previous test contributing (I know calls are meant to be reset between tests).
This test was running just fine when I was on 1.5, so this has to be something with the way my code\test is written not playing nicely with changes in 1.6.x
Can someone shed some light on what may be going on here please?
Thanks
I didnt have the .catch(angular.noop) parts in prior to upgrading to 1.6.3 and came across some posts suggesting to do this in order to make the tests happy, which certainly helped for me in getting past the unhandled rejection error in my test run.
Adding .catch(angular.noop) will certainly handle the unhandled rejection.
It converts the rejected promise to a fulfilled promise!!
Your test is correctly failing because you broke your code.
For more information, see Catch method not working with $http get request
Changes to $q for AngularJS V1.6
report promises with non rejection callback
Rejected promises that do not have a callback to handle the rejection report
this to $exceptionHandler so they can be logged to the console.
BREAKING CHANGE
Unhandled rejected promises will be logged to $exceptionHandler.
Tests that depend on specific order or number of messages in $exceptionHandler
will need to handle rejected promises report.
treat thrown errors as regular rejections
Previously, errors thrown in a promise's onFulfilled or onRejected handlers were treated in a
slightly different manner than regular rejections:
They were passed to the $exceptionHandler() (in addition to being converted to rejections).
The reasoning for this behavior was that an uncaught error is different than a regular rejection, as
it can be caused by a programming error, for example. In practice, this turned out to be confusing
or undesirable for users, since neither native promises nor any other popular promise library
distinguishes thrown errors from regular rejections.
(Note: While this behavior does not go against the Promises/A+ spec, it is not prescribed either.)
This commit removes the distinction, by skipping the call to $exceptionHandler(), thus treating
thrown errors as regular rejections.
Note:
Unless explicitly turned off, possibly unhandled rejections will still be caught and passed to the
$exceptionHandler(), so errors thrown due to programming errors and not otherwise handled (with a
subsequent onRejected handler) will not go unnoticed.
For more information, see AngularJS Developer Guide - Migrating from V1.5 to V1.6
disable Possibly Unhandled Rejection by this config and test again.
app.config(['$qProvider', function ($qProvider) {
$qProvider.errorOnUnhandledRejections(false);
}]);

Angular: Get aware of spelling mistakes in function call with ng-click

My question is about discovering possible spelling mistakes in angular expressions, in particular spelling mistakes in the function name.
Consider the snippet bellow:
I have two buttons there, the first one with correct spelling, the second with a spelling mistake in the angular expression. Clicking the second button does nothing and gives no hints about a potential error.
My question is now: are there ways to detect erroneous calls to function that don't exist (while executing the application)?
I am not looking for some checking possibility in the build or unit test process but rather would like to see a way I could get aware of such a potential issue when running the erroneous expression in the browser when the application is executed.
angular.module("myApp", [])
.controller("TestController", function($scope){
$scope.myFunction = function() {
console.log("Hello World");
};
});
angular.element(document).ready(function () {
angular.bootstrap(document, ['myApp']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
<section ng-controller="TestController">
<button ng-click="myFunction()">myFunction</button>
<button ng-click="myFunctio()">myFunctio</button>
</section>
I'm not familiar with a built in option in angular to do that (using binding to an "undefined" object is a legit UC as things may become "undefined" during program run) - but you may write your own "ng-click" directive which, in case not finding the function to bound to, raise an error (exception or better - console error / warning).
This is an extremely common complaint about Angular. Even when writing code for the Closure compiler, with all the type annotations and everything, these still fall right through the cracks.
You can kluge something together, I've seen things like bussing all events to a common broker and looking for the target handler in the bound scope, and so on. But it always appears to be more trouble than it's worth.
Your unit tests are where you catch this sort of thing. It's why being able to test template code via triggering events is such an important thing for an Angular developer to master. If you trigger that button click and your test fails (e.g. your spyOn the handler never gets called), check the template.
Protractor (and other end to end testing frameworks) will do that for you.
I'm not sure if this would work for function calls or not, but it would solve part of the problem of misspelling something. In Scott Allen's AngularJS Playbook course on Pluralsight, he suggests creating a decorator for the $interpolate service to see if any bindings are potentially incorrect. Here is the code for that:
(function(module) {
module.config(function ($provide) {
$provide.decorator("$interpolate", function ($delegate, $log) {
var serviceWrapper = function () {
var bindingFn = $delegate.apply(this, arguments);
if (angular.isFunction(bindingFn) && arguments[0]) {
return bindingWrapper(bindingFn, arguments[0].trim());
}
return bindingFn;
};
var bindingWrapper = function (bindingFn, bindingExpression) {
return function () {
var result = bindingFn.apply(this, arguments);
var trimmedResult = result.trim();
var log = trimmedResult ? $log.info : $log.warn;
log.call($log, bindingExpression + " = " + trimmedResult);
return result;
};
};
angular.extend(serviceWrapper, $delegate);
return serviceWrapper;
});
});
}(angular.module("common")));

Expose object fron Angularjs App to Protractor test

I am writing end-to-end tests for my AngularJS-based application using Protractor. Some cases require using mocks to test - for example, a network connection issue. If an AJAX request to server fails, the user must see a warning message.
My mocks are registered in the application as services. I want them to be accessible to the tests to write something like this:
var proxy;
beforeEach(function() { proxy = getProxyMock(); });
it("When network is OK, request succeeds", function(done) {
proxy.networkAvailable = true;
element(by.id('loginButton')).click().then(function() {
expect(element(by.id('error')).count()).toEqual(0);
done();
});
});
it("When network is faulty, message is displayed", function(done) {
proxy.networkAvailable = false;
element(by.id('loginButton')).click().then(function() {
expect(element(by.id('error')).count()).toEqual(1);
done();
});
});
How do I implement the getProxyMock function to pass an object from the application to the test? I can store proxies in the window object of the app, but still do not know how to access it.
After some reading and understanding the testing process a bit better, it turned to be impossible. The tests are executed in NodeJS, and the frontend code in a browser - Javascript object instances cannot be truly shared between two different processes.
However, there is a workaround: you can execute a script inside browser.
First, your frontend code must provide some sort of service locator, like this:
angular.module('myModule', [])
.service('proxy', NetworkProxy)
.run(function(proxy) {
window.MY_SERVICES = {
proxy: proxy,
};
});
Then, the test goes like this:
it("Testing the script", function(done) {
browser.executeScript(function() {
window.MY_SERVICES.proxy.networkAvailable = false;
});
element(by.id('loginButton')).click().then(function() {
expect(element.all(by.id('error')).count()).toEqual(1);
done();
});
});
Please note that when you use executeScript, the function is serialized to be sent to browser for execution. This puts some limitations worth keeping in mind: if your script function returns a value, it is a clone of the original object from browser. Updating the returned value will not modify the original! For the same reason, you cannot use closures in the function.

Promises are not resolved in Jasmine using Karma

I have a problem with a small karma unit test that should check a simple decryption/encryption service.
The thing is, if I call the following code "manual" (i.e., within my running angular app) everything is fine and I receive the expected test output:
this.encryptDataAsync('Hello World of Encryption','b4b63cd1a64dbef72fefe2eb3e3fc3eb').then((encryptedValue : string) : void => {
console.log('1',encryptedValue);
this.decryptDataAsync(encryptedValue,'b4b63cd1a64dbef72fefe2eb3e3fc3eb').then(function(decryptedValue : string) : void{
console.log('2',decryptedValue);
});
});
As soon as I try to run this Karma/Jasmine unit test
describe('simple encryption/decryption', function() {
var results = '';
beforeEach(function(done) {
_cryptoService.encryptDataAsync('ABC','b4b63cd1a64dbef72fefe2eb3e3fc3eb').then(function (encryptedValue){
console.log('1');
_cryptoService.decryptDataAsync(encryptedValue,'b4b63cd1a64dbef72fefe2eb3e3fc3eb').then(function(decryptedValue){
console.log('2');
results = decryptedValue;
done();
});
});
});
it("check results", function(done){
expect(results).toBe('ABC');
done();
}, 3000);
});
I never reach console.log('1') nor '2'. I can confirm this while debugging the unit test. However, this is the only unit test that fails in the complete suite, so I guess it won't by a problem with modules, etc.
Is there a general problem with my test case? I would have expected that I can use the then functions to handle my test case and, afterwards, call the done() function to invoke the assertion part.
Update/Edit:
The service uses webcrypto as a library. It is complete independent of angular besides being an angular service (so, no variables on scopes, etc)
I needed to call scope.apply since "$q is integrated with the $rootScope.Scope Scope model observation mechanism in angular, which means faster propagation of resolution or rejection into your models and avoiding unnecessary browser repaints, which would result in flickering UI."

angularJS unit testing where run contains a HTTP request?

I am fairly new to AngularJS and am trying to learn some best practices. I have things working, but would like to start adding some unit tests to my modules and controllers. The first one I am looking to tackle is my AuthModule.
I have an AuthModule. This Module registers a Factory called "AuthModule" and exposes things like "setAuthenticatedUser" and also fields like "isLoggedIn" and "currentUser". I think this is a fairly common pattern in an AngularJS application, with some variations on the specific implementation details.
authModule.factory(
'AuthModule',
function(APIService, $rootScope) {
var _currentUser = null;
var _isLoggedIn = false;
return {
'setAuthenticatedUser' : function(currentUser) {
_currentUser = currentUser;
_isLoggedIn = currentUser == null ? false : true;
$rootScope.$broadcast('event:authenticatedUserChanged',
_currentUser);
if (_isLoggedIn == false) {
$rootScope.$broadcast('event:loginRequired')
}
$rootScope.authenticatedUser = _currentUser;
$rootScope.isLoggedIn = _isLoggedIn;
},
'isLoggedIn' : _isLoggedIn,
'currentUser' : _currentUser
}
});
The module does some other things like register a handler for the event "loginRequired" to send the person back to the home screen. These events are raised by the AuthModule factory.
authModule.run(function($rootScope, $log, $location) {
$rootScope.$on("event:loginRequired", function(event, data) {
$log.info("sending him home. Login is required");
$location.path("/");
});
});
Finally, the module has a run block which will use an API service I have to determine the current logged in user form the backend.
authModule.run(
function(APIService, $log, AuthModule) {
APIService.keepAlive().then(function(currentUser) {
AuthModule.setAuthenticatedUser(currentUser.user);
}, function(response) {
AuthModule.setAuthenticatedUser(null);
});
});
Here are some of my questions:
My question is how would you setup tests for this? I would think that I would need to Mock out the APIService? I'm having a hard time because I keep getting unexpected POST request to my /keepalive function (called within APIService.keepAlive())?
Is there any way to use $httpBackend in order to return the right response to the actual KeepAlive call? This would prevent me from having to mock-out the API service?
Should I pull the .run() block out which obtains the current logged in user out of the AuthModule and put it into the main application? It seems no matter where I put the run() block, I can't seem to initialize the $httpbackend before I load the module?
Should the AuthModule even be its own module at all? or should I just use the main application module and register the factory there?
Run blocks are the closest thing in Angular to the main method. A run block is the code which needs to run to kickstart the application. It is executed after all of the service have been configured and the injector has been created. Run blocks typically contain code which is hard to unit-test, and for this reason should be declared in isolated modules, so that they can be ignored in the unit-tests.angularjs docs
I suggest you take a look at this authentication service, using a service is the way to go.
Hopefully this would help ... Good luck

Resources