Testing asynchronous $rootScope broadcasts with controller using $on - angularjs

I have code similar to this...
//controller
function getPromise = function(){
return service.getPromise();
}
$rootScope.$on('event', function(){
return getPromise();
});
I am trying to create a jasmine test for the rootScope event. I can test the function itself (if I expose it) by using a .then and checking for the result, but I can't figure out how to do it through the on call.

Related

How to unit test / mock a $timeout call?

How do I mock the timeout call, here?
$scope.submitRequest = function () {
var formData = getData();
$scope.form = JSON.parse(formData);
$timeout(function () {
$('#submitForm').click();
}, 2000);
};
I want to see timeout has been called with the correct function.
I would like an example of the spyon function mocking $timeout.
spyOn(someObject,'$timeout')
First of all, DOM manipulation should only be performed in directives.
Also, it's better to use angular.element(...), than $(...).
Finally, to do this, you can expose your element's click handler to the scope, spy on it, and check if that handler has been called:
$timeout.flush(2000);
$timeout.verifyNoPendingTasks();
expect(scope.myClickHandler).toHaveBeenCalled();
EDIT:
since that's a form and there is no ng-click handler, you can use ng-submit handler, or add a name to your form and do:
$timeout.flush(2000);
$timeout.verifyNoPendingTasks();
expect(scope.formName.$submitted).toBeTruthy();
$timeout can be spied or mocked as shown in this answer:
beforeEach(module('app', ($provide) => {
$provide.decorator('$timeout', ($delegate) => {
var timeoutSpy = jasmine.createSpy().and.returnValue($delegate);
// methods aren't copied automatically to spy
return angular.extend(timeoutSpy, $delegate);
});
}));
There's not much to test here, since $timeout is called with anonymous function. For testability reasons it makes sense to expose it as scope/controller method:
$scope.submitFormHandler = function () {
$('#submitForm').click();
};
...
$timeout($scope.submitFormHandler, 2000);
Then spied $timeout can be tested:
$timeout.and.stub(); // in case we want to test submitFormHandler separately
scope.submitRequest();
expect($timeout).toHaveBeenCalledWith(scope.submitFormHandler, 2000);
And the logic inside $scope.submitFormHandler can be tested in different test.
Another problem here is that jQuery doesn't work well with unit tests and requires to be tested against real DOM (this is one of many reasons why jQuery should be avoided in AngularJS applications when possible). It's possible to spy/mock jQuery API like shown in this answer.
$(...) call can be spied with:
var init = jQuery.prototype.init.bind(jQuery.prototype);
spyOn(jQuery.prototype, 'init').and.callFake(init);
And can be mocked with:
var clickSpy = jasmine.createSpy('click');
spyOn(jQuery.prototype, 'init').and.returnValue({ click: clickSpy });
Notice that it's expected that mocked function will return jQuery object for chaining with click method.
When $(...) is mocked, the test doesn't require #submitForm fixture to be created in DOM, this is the preferred way for isolated unit test.
Create mock for $timeout provider:
var f = () => {}
var myTimeoutProviderMock = () => f;
Use it:
beforeEach(angular.mock.module('myModule', ($provide) => {
$provide.factory('$timeout', myTimeoutProviderMock);
}))
Now you can test:
spyOn(f);
expect(f).toHaveBeenCalled();
P.S. you'd better test result of function in timeout.
Assuming that piece of code is within the controller or being created in the test by $controller, then $timeout can be passed in the construction parameter. So you could just do something like:
var timeoutStub = sinon.stub();
var myController = $controller('controllerName', timeoutStub);
$scope.submitRequest();
expect(timeoutStub).to.have.been.called;
Unit Tesitng $timeout with flush delay
You have to flush the queue of the $timeout service by calling $timeout.flush()
describe('controller: myController', function(){
describe('showAlert', function(){
beforeEach(function(){
// Arrange
vm.alertVisible = false;
// Act
vm.showAlert('test alert message');
});
it('should show the alert', function(){
// Assert
assert.isTrue(vm.alertVisible);
});
it('should hide the alert after 5 seconds', function(){
// Act - flush $timeout queue to fire off deferred function
$timeout.flush();
// Assert
assert.isFalse(vm.alertVisible);
});
})
});
Please checkout this link http://jasonwatmore.com/post/2015/03/06/angularjs-unit-testing-code-that-uses-timeout
I totally agree with Frane Poljak's answer. You should surely follow his way. Second way to do it is by mocking $timeout service like below:
describe('MainController', function() {
var $scope, $timeout;
beforeEach(module('app'));
beforeEach(inject(function($rootScope, $controller, $injector) {
$scope = $rootScope.$new();
$timeout = jasmine.createSpy('$timeout');
$controller('MainController', {
$scope: $scope,
$timeout: $timeout
});
}));
it('should submit request', function() {
$scope.submitRequest();
expect($timeout).toHaveBeenCalled();
});
Here is the plunker having both approaches: http://plnkr.co/edit/s5ls11

confused about the need for $scope.$apply

I have an angular controller:
.controller('DashCtrl', function($scope, Auth) {
$scope.login = function() {
Auth.login().then(function(result) {
$scope.userInfo = result;
});
};
});
Which is using a service I created:
.service('Auth', function($window) {
var authContext = $window.Microsoft.ADAL.AuthenticationContext(...);
this.login = function() {
return authContext.acquireTokenAsync(...)
.then(function(authResult) {
return authResult.userInfo;
});
};
});
The Auth service is using a Cordova plugin which would be outside of the angular world. I guess I am not clear when you need to use a $scope.$apply to update your $scope and when you don't. My incorrect assumption was since I had wrapped the logic into an angular service then I wouldn't need it in this instance, but nothing gets updated unless I wrap the $scope.userInfo = statement in a $timeout or $scope.$apply.
Why is it necessary in this case?
From angular's wiki:
AngularJS provides wrappers for common native JS async behaviors:
...
jQuery.ajax() => $http
This is just a traditional async function with a $scope.$apply()
called at the end, to tell AngularJS that an asynchronous event just
occurred.
So i guess since your Auth service does not use angular's $http, $scope.$apply() isn't called by angular after executing the Async Auth function.
Whenever possible, use AngularJS services instead of native. If you're
creating an AngularJS service (such as for sockets) it should have a
$scope.$apply() anywhere it fires a callback.
EDIT:
In your case, you should trigger the digest cycle once the model is updated by wrapping (as you did):
Auth.login().then(function(result) {
$scope.$apply(function(){
$scope.userInfo = result;
});
});
Or
Auth.login().then(function(result) {
$scope.userInfo = result;
$scope.$apply();
});
Angular does not know that $scope.userInfo was modified, so the digest cycle needs to be executed via the use of $scope.$apply to apply the changes to $scope.
Yes, $timeout will also trigger the digest cycle. It is simply the Angular version of setTimeout that will execute $scope.$apply after the wrapped code has been run.
In your case, $scope.$apply() would suffice.
NB: $timeout also has exception handling and returns a promise.

spyOn $scope.$on after $broadcast toHaveBeenCalled fails

I'm having a lot of trouble getting this simple test working.
I've got an $scope.$on listener in a controller that I want to test. I just want to make certain it's called after a broadcast event.
To do this, I thought the following code would work:
describe("Testing the parent controller: ", function() {
var scope, ctrl;
beforeEach(function() {
module("myApp");
inject(function($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('parent-ctrl', {
$scope: scope,
});
});
});
it ("should trigger broadcast when current page updates", function() {
spyOn(scope, "$on");
scope.$broadcast("myEvent", 999);
expect(scope.$on).toHaveBeenCalled();
});
});
It doesn't (Expected spy $on to have been called.). I've dug through numerous examples:
How do I test an event has been broadcast in AngularJS?
in-angularjs
How do I test $scope.$on in AngularJS
How can I test events in angular?
unit test spy on $emit
How do I unit test $scope.broadcast, $scope.$on using Jasmine
How do I test $scope.$on in AngularJS
How can I test Broadcast event in AngularJS
and learned a lot, but for some reason I'm just not making some critical connection.
I have noticed that the $on handler does respond post-assertion, which is unhelpful. I've tried scope.$apply() and .andCallThrough() in various configurations but nothing seems to work.
How is this done?
When the event is broadcasted it is the listener function that was registered with $on that is executed, not the $on function itself.
Your current test would work for code like this, which is probably not what you have:
$scope.$on('myEvent', function () {
$scope.$on('whatever', someFn);
});
What you should be testing is whatever your registered listener function is doing.
So if you for example have:
$scope.$on('myEvent', function() {
myFactory.doSomething();
});
Test it like this:
spyOn(myFactory, "doSomething");
scope.$broadcast("myEvent");
expect(myFactory.doSomething).toHaveBeenCalled();

How can I test data set in a service callback when testing a controller in AngularJS?

Lets say I have a service which queries some data and sets it in the controller, a little similar to:
(Method on controller)
DogService.query(function(data)){
if(data.isSuccess){
$scope.IloveDogs = true;
$scope.dogLovers += 1;
}
})
It is highly simplified, but how would I in my controller test that when calling a mocked dogService, that it sets the correct data?
If for simplicity we say that the function isn't asynchronous and deals with promises, I would create and inject a mock to the controller. The mock could look like:
var DogService = {
query: function(){
return true;
}
}
This unfortunately doesn't run the code where the $scope.IloveDogs is set to true, and the dogLovers is incremented by one.
Any ideas, since I would rather not have to duplicate the code in my controller from the service to the mocked service?
This is how I would normally mock a service in a unit test.
(You didn't mention which testing framework you use, so I am going to assume Jasmine as it's the most popular one at the moment).
I just create a dumb object to act as my mock and then just Jasmine's built-in spy functionality to dictate what it returns. Note that this is syntax for Jasmine 2.0.
I use $q to create a promise, and make sure I am able to reference it from my tests so I can resolve it.
describe('Spec', function() {
var scope;
var catServiceMock;
var deferredCatCall;
beforeEach(module('myModule'));
beforeEach(inject(function($controller, $rootScope, $q) {
scope = $rootScope;
//Create a mock and spy on it to return a promise
deferredCatCall = $q.defer();
catServiceMock = {
query: function() {}
};
spyOn(catServiceMock, 'query').and.returnValue(deferredCatCall.promise);
//Inject the mock into the controller
$controller('MyCtrl', {
$scope: scope,
catService: catServiceMock
});
}));
it('proves that cats are better than dogs', function() {
//resolve the promise that was returned by the mock
deferredCatCall.resolve({
isSuccess: true
});
//Need to trigger a $digest loop so angular process the resolved promise
scope.$digest();
//Check that the controller callback did something
expect(scope.iLoveCats).toBeTruthy();
});
});
For a service that does not use promises, I would possibly do something like this:
describe('Spec', function() {
var scope;
var catServiceMock;
beforeEach(module('myModule'));
beforeEach(inject(function($controller, $rootScope, $q) {
scope = $rootScope;
//Create a mock and spy on it to return a value
catServiceMock = {
query: function() {}
};
spyOn(catServiceMock, 'query').and.returnValue({
isSuccess: true
});
//Inject the mock into the controller
$controller('MyCtrl', {
$scope: scope,
catService: catServiceMock
});
}));
it('proves that cats are better than dogs', function() {
//Check that the controller callback did something
expect(scope.iLoveCats).toBeTruthy();
});
});
The main problem with this approach is that you're forced to dictate what the service will return before you instantiate the controller. This means that if you want to test how the controller behaves to different data received from the service you're going to have to have multiple beforeEach blocks nested in different describe blocks and while it looks at a glance like it's less boilerplate in the test you will end up with a lot more.
This is one of the reasons why I prefer my services to return promises even if they are not asynchronous.

AngularJS: spyOn both $timeout and $timeout.cancel

When testing part of an AngularJS Application which uses both $timeout and $timeout.cancel with Jasmine's spyOn method.
describe('<whatever>', function() {
beforeEach(function() {
spyOn(this, '$timeout').andCallThrough();
spyOn(this.$timeout, 'cancel').andCallThrough();
this.createController();
});
it('should <whatever>', function() {
expect(this.$timeout).toHaveBeenCalled();
expect(this.$timeout.cancel).toHaveBeenCalled();
});
});
You should encounter the following error in your application code, which is using what your test injected into it.
TypeError: 'undefined' is not a function (evaluating '$timeout.cancel(timeoutPromise)');
If you were to run console.log(Object.keys(this.$timeout)); in your test suite, you will see the following output;
LOG: ['identity', 'isSpy', 'plan', 'mostRecentCall', 'argsForCall', 'calls', 'andCallThrough', 'andReturn', 'andThrow', 'andCallFake', 'reset', 'wasCalled', 'callCount', 'baseObj', 'methodName', 'originalValue']
$timeout is a function which AngularJS is also decorating—since functions are objects—with a cancel method. Because this isn't that common a thing to do, Jasmine replaces rather than augments $timeout with it's spying implementation - clobbering $timeout.cancel.
A workaround for this is to put the cancel spy back again after $timeout has been overwritten by Jasmine's $timeout spy, as follows;
describe('<whatever>', function() {
beforeEach(function() {
spyOn(this.$timeout, 'cancel').andCallThrough();
var $timeout_cancel = this.$timeout.cancel;
spyOn(this, '$timeout').andCallThrough();
this.$timeout.cancel = $timeout_cancel;
this.createController();
});
it('should <whatever>', function() {
expect(this.$timeout).toHaveBeenCalled();
expect(this.$timeout.cancel).toHaveBeenCalled();
});
});
This worked for me for $interval, so it should work for $timeout. (jasmine 2)
var $intervalSpy = jasmine.createSpy('$interval', $interval).and.callThrough();
Then I can do both:
expect($intervalSpy.cancel).toHaveBeenCalledTimes(1);
expect($intervalSpy).toHaveBeenCalledTimes(1);

Resources