Angularjs unit test watch callback not getting called even with scope.$digest - angularjs

I'm struggling unit testing a controller that watches a couple variables. In my unit tests, I can't get the callback for the $watch function to be called, even when calling scope.$digest(). Seems like this should be pretty simple, but I'm having no luck.
Here's what I have in my controller:
angular.module('app')
.controller('ClassroomsCtrl', function ($scope, Classrooms) {
$scope.subject_list = [];
$scope.$watch('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
$scope.classrooms = Classrooms.search(ctrl.functions.search_params());
}
});
});
And here's my unit test:
angular.module('MockFactories',[]).
factory('Classrooms', function(){
return jasmine.createSpyObj('ClassroomsStub', [
'get','save','query','remove','delete','search', 'subjects', 'add_subject', 'remove_subject', 'set_subjects'
]);
});
describe('Controller: ClassroomsCtrl', function () {
var scope, Classrooms, controllerFactory, ctrl;
function createController() {
return controllerFactory('ClassroomsCtrl', {
$scope: scope,
Classrooms: Classrooms
});
}
// load the controller's module
beforeEach(module('app'));
beforeEach(module('MockFactories'));
beforeEach(inject(function($controller, $rootScope, _Classrooms_ ){
scope = $rootScope.$new();
Classrooms = _Classrooms_;
controllerFactory = $controller;
ctrl = createController();
}));
describe('Scope: classrooms', function(){
beforeEach(function(){
Classrooms.search.reset();
});
it('should call Classrooms.search when scope.subject_list changes', function(){
scope.$digest();
scope.subject_list.push(1);
scope.$digest();
expect(Classrooms.search).toHaveBeenCalled();
});
});
});
I've tried replacing all the scope.$digest() calls with scope.$apply() calls. I've tried calling them 3 or 4 times, but I can't get the callback of the $watch to get called.
Any thoughts as to what could be going on here?
UPDATE:
Here's an even simpler example, that doesn't deal with mocks, stubbing or injecting factories.
angular.module('edumatcherApp')
.controller('ClassroomsCtrl', function ($scope) {
$scope.subject_list = [];
$scope.$watch('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
And unit test:
it('should set scope.test', function(){
scope.$digest();
scope.subject_list.push(1);
scope.$digest();
expect(scope.test).toBeDefined();
});
This fails too with "Expected undefined to be defined." and nothing is logged to the console.
UPDATE 2
2 more interesting things I noted.
It seems like one problem is that newValue and oldValue are the same when the callback is called. I logged both to the console, and they are both equal to []. So, for example, if I change my $watch function to look like this:
$scope.$watch('subject_list', function(newValue, oldValue){
console.log('testing');
$scope.test = 'test';
});
the test passes fine. Not sure why newValue and oldValue aren't getting set correctly.
If I change my $watch callback function to be a named function, and just check to see if the named function is ever called, that fails as well. For example, I can change my controller to this:
$scope.update_classrooms = function(newValue, oldValue){
$scope.test = 'testing';
console.log('test');
};
$scope.watch('subject_list', $scope.update_classrooms);
And change my test to this:
it('should call update_classrooms', function(){
spyOn(scope,'update_classrooms').andCallThrough();
scope.$digest();
scope.subject_list.push(1);
scope.$digest();
expect(scope.update_classrooms).toHaveBeenCalled();
});
it fails with "Expected spy update_classrooms to have been called."
In this case, update_classrooms is definitely getting called, because 'test' gets logged to the console. I'm baffled.

I just ran into this problem within my own code base and the answer turned out to be that I needed a scope.$digest() call right after instantiating the controller. Within your tests you have to call scope.$digest() manually after each change in watched variables. This includes after the controller is constructed to record the initial watched value(s).
In addition, as Vitall specified in the comments, you need $watchCollection() for collections.
Changing this watch in your simple example resolves the issue:
$scope.$watch('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
to:
$scope.$watchCollection('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
I made a jsfiddle to demonstrate the difference:
http://jsfiddle.net/ydv8k4zy/ - original with failing test - fails due to using $watch, not $watchCollection
http://jsfiddle.net/ydv8k4zy/1/ - functional version
http://jsfiddle.net/ydv8k4zy/2/ - $watchCollection fails without the initial scope.$digest.
If you play around With console.log on the second failing item you'll see that the watch is called with the same value for old and new after the scope.$digest() (line 25).

The reason that this isn't testable is because the way that the functions are passed into the watchers vs the way that spyOn works. The spyOn method takes the scope object and replaces the original function on that object with a new one... but most likely you passed the whole method by reference into the $watch, that watch still has the old method. I created this non-AngularJS example of the same thing:
https://jsfiddle.net/jonhartmann/9bacozmg/
var sounds = {};
sounds.beep = function () {
alert('beep');
};
document.getElementById('x1').addEventListener('click', sounds.beep);
document.getElementById('x2').addEventListener('click', function () {
sounds.beep = function () {
alert('boop');
};
});
document.getElementById('x3').addEventListener('click', function () {
sounds.beep();
});
The difference between the x1 handler and the x3 handler is that in the first binding the method was passed in directly, in the second the beep/boop method just calls whatever method is on the object.
This is the same thing I ran into with my $watch - I'd put my methods on a "privateMethods" object, pass it out and do a spyOn(privateMethods, 'methodname'), but since my watcher was in the format of $scope.$watch('value', privateMethods.methodName) it was too late - the code would get executed by my spy wouldn't work. Switching to something like this skipped around the problem:
$scope.$watch('value', function () {
privateMethods.methodName.apply(null, Array.prototype.slice.call(arguments));
});
and then the very expected
spyOn(privateMethods, 'methodName')
expect(privateMethods.methodName).toHaveBeenCalled();
This works because you're no longer passing privateMethods.methodName by reference into the $watch, you're passing a function that in turn executes the "methodName" function on "privateMethods".

Your problem is that you need to call $scope.$watchCollection instead of plain $watch.
$watch will only respond to assignments (i.e scope.subject_list = ['new item']).
In addition to assignments, $watchCollection will respond to changes to lists (push/splice).

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

Jasmine (Karma) test failing with $rootScope.$watch

I'm trying to test a function in my controller that first watches for a dictionary to be loaded before it takes any action.
The problem i am getting is that my test fails because the watch doesn't appear to run.
My Controller Function
function setPageTitle(title) {
$rootScope.$watch('dictionary', function(dictionary) {
if (dictionary) {
if ($location.$$path != '/dashboard') {
$rootScope.pageTitle = $rootScope.dictionary.pageTitles[title] || $rootScope.dictionary.pageTitles.dashboard || 'Dashboard';
} else {
$rootScope.pageTitle = $rootScope.dictionary.pageTitles.dashboard || 'Dashboard';
}
}
});
}
My Test...
describe('AppController function', function() {
var rootScope, scope, $location, $window, controller, createController, cacheFactory, toastr;
beforeEach(module('mockedDashboard'));
beforeEach(inject(function(_$rootScope_, $controller, _$location_, _$window_, _toastr_, _$timeout_) {
$location = _$location_;
$window = _$window_;
$timeout = _$timeout_;
scope = _$rootScope_.$new();
rootScope = _$rootScope_;
toastr = _toastr_;
createController = function() {
return $controller('AppController', {
'$scope': scope
});
};
controller = createController();
}));
// We are using CacheFactory in this project, when running multiple tests on the controller
// we need to destroy the cache for each test as the controller is initialized for each test.
afterEach(inject(function(_CacheFactory_) {
cacheFactory = _CacheFactory_;
cacheFactory.destroy('defaultCache');
}));
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function() {
scope.setPageTitle('logIn');
scope.$apply();
expect(rootScope.pageTitle).toBe('LOG IN');
});
});
});
the failure message
Expected undefined to be 'LOG IN'
rootScope.pageTitle is never set because the watch doesn't run when the test calls the function. How can i get around this?
I tried scope.$apply() which i read should trigger the $watch, but it still doesn't work.
EDIT
I tried using the done function, however the test still fails because rootScope.pageTitle still appears to remain undefined. 3000ms should be ample time for this to work, usually this is done in less that 500ms. (i also know that the code works because this test is being written too late)
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function(done) {
scope.setPageTitle('logIn');
scope.$apply();
setTimeout(function() {
// console.log('dictionary: ' + rootScope.dictionary.pageTitles.logIn);
console.log('rootScope.pageTitle: ' + rootScope.pageTitle);
expect(rootScope.pageTitle).toBe('LOG IN');
// expect(true).toBe(false);
done();
}, 3000);
});
});
Well, it is pretty obvious - your test is failing because the code ... is not running.
$rootScope.$watch('dictionary', function(dictionary) {
if (dictionary) {
...
}
});
Most probably the problem is in $rootScope.dictionary being undefined due to not being explicitly set for testing purposes.
Try this
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function() {
scope.setPageTitle('logIn');
rootScope.dictionary = {
'whatever you': 'need here'
};
scope.$apply();
expect(rootScope.pageTitle).toBe('LOG IN');
});
});
And by the way - there is absolutely no need in using async test case, as $digest will be invoked syncronously at scope.$apply(); line.
It looks like your test is a synchronous test, and therefore Karma thinks it's done as soon as the function returns. Unfortunately, this isn't actually the case since you need to wait around for the affects of the $apply to occur, which takes another pass through the event loop. You'll want to turn your test into an asynchronous test by adding a done parameter and calling it somehow after your watch has been triggered.
it('description', function(done) {
// do your setup things here
scope.$apply();
setTimeout(function() {
// make your assertions here
done()
}, 25);
})
Of course, using setTimeout is a pretty ugly way to wait the necessary time, and if there's some other event you can watch for, it would be more elegant.
Few small changes done on your test case, you can give a try for this,
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function() {
rootScope.dictionary = "someValue"; // As you were watching this object assign some value and then apply $digest
// Instead of $apply use $digest with rootscope
rootscope.$digest();
expect(rootScope.pageTitle).toBe('LOG IN');
});
});

Angular - Jasmine: Testing immediately invoked functions inside a directive controller

Suppose that I have a directive like:
.directive('myDir', function(TemplateHandler){
return {
...
controller: function(ExploreCmd){
this.foo = {
bar: function(){...}
};
this.foo.bar();
}
}
});
And I want to test that when the directive is loaded the this.foo.bar() function has been called, how can I achieve that?
I tried with:
beforeEach(inject(function($compile, $rootScope){
scope = $rootScope.$new();
element = angular.element('<js-shell></js-shell>');
$compile(element)(scope);
scope.$digest();
isolatedScope = element.isolateScope().vm;
spyOn(isolatedScope.foo, 'bar');
}));
it('should register the explore command', () => {
expect(isolatedScope.foo.bar).toHaveBeenCalled();
});
But the problem is that spyOn(isolatedScope.foo, 'bar'); is called after the isolatedScope.foo.bar has been called, so the test fail.
I don't think you can in this situation. There is no moment in time between when foo is created and when foo.bar is invoked where you could get hold of it to replace foo.bar with a spy.
By the time your spyOn function runs, foo.bar() has been called and returned.
If foo.bar has a side effect like setting foo.baz to true, you can look at that.

Jasmine Js - SpyOn Fakecall during the controller initialization

I have seen a set of duplicates for this question but was unable to solve the issue.
I have a controller and during the controller initialization, fetchtemplate() is getting called first and then my mock fetchtemplate() is getting called.
How do I stop the actual(controller) fetchtemplate() getting called during the controller initialization? My intention is to mock the function fetchtemplate() in my spec.Please have a look at my spec -
describe("...",function(){
beforeEach(inject(function($controller,...) {
scope = $rootScope.$new();
this.init = function() {
$controller('ChangeControlCreateController', {
$scope: scope
});
}
}));
describe('Function', function() {
it("-- check for trueness",function(){
this.init() ; //Initialization of the controller
spyOn(scope,'fetchtemplate').and.callFake(function() {
return 101;
});
var fakeResponse = scope.fetchtemplate();
expect(scope.fetchtemplate).toHaveBeenCalled();
expect(fakeResponse).toEqual(101);
});
});
});
I have tried placing the spyOn before the this.init() which gave error as the fetchtemplate() doesn't exist at that time to spyOn.
My controller code structure looks like -
angular.module('...', [...])
.controller('ChangeControlCreateController', ["$scope"...,
function ChangeControlCreateController($scope,...) {
$scope.fetchtemplate = function() {
console.log("controller's function");
...
};
$scope.fetchtemplate();
});
The result what I am getting is - First the console item "controller's function" and then the spec is executing with mock function. I want the mock function to execute without the controller's function to execute
So if I understand correctly you are doing some call to a function that is doing something you want to prevent for test purposes. Probably an http call or some thing of the sort ?
Whatever it is doing the proper way to handle something like that is usually to put that method inside a service instead and then to spy on that service method. Here is an example of test if the service is TemplateService :
describe("...",function(){
var $controller, scope, TemplateService, YourController;
beforeEach(inject(function(_$controller_, _TemplateService_, ...) {
scope = $rootScope.$new();
$controller = _$controller_;
TemplateService = _TemplateService_;
}
it("-- check for trueness",function(){
spyOn(TemplateService,'fetchTemplate').and.returnValue('101');
YourController = $controller('YourController');
expect(...);
});
});
I hope that's helpful

Spy on scope function that executes when an angular controller is initialized

I want to test that the following function is in fact called upon the initialization of this controller using jasmine. It seems like using a spy is the way to go, It just isn't working as I'd expect when I put the expectation for it to have been called in an 'it' block. I'm wondering if there is a special way to check if something was called when it wasn't called within a scope function, but just in the controller itself.
App.controller('aCtrl', [ '$scope', function($scope){
$scope.loadResponses = function(){
//do something
}
$scope.loadResponses();
}]);
//spec file
describe('test spec', function(){
beforeEach(
//rootscope assigned to scope, scope injected into controller, controller instantiation.. the expected stuff
spyOn(scope, 'loadResponses');
);
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function(){
expect(scope.loadResponses).toHaveBeenCalled();
});
});
You need to initialise the controller yourself with the scope you've created. The problem is, that you need to restructure your code. You can't spy on a non-existing function, but you need to spyOn before the function gets called.
$scope.loadResponses = function(){
//do something
}
// <-- You would need your spy attached here
$scope.loadResponses();
Since you cannot do that, you need to make the $scope.loadResponses() call elsewhere.
The code that would successfully spy on a scoped function is this:
var scope;
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', {$scope: scope});
scope.$digest();
}));
it("should have been called", function() {
spyOn(scope, "loadResponses");
scope.doTheStuffThatMakedLoadResponsesCalled();
expect(scope.loadResponses).toHaveBeenCalled();
});
Setting the spy before controller instantiation (in the beforeEach) is the way to test controller functions that execute upon instantiation.
EDIT: There is more to it. As a comment points out, the function doesn't exist at the time of ctrl instantiation. To spy on that call you need to assign an arbitrary function to the variable (in this case you assign scope.getResponses to an empty function) in your setup block AFTER you have scope, but BEFORE you instantiate the controller. Then you need to write the spy (again in your setup block and BEFORE ctrl instantiation), and finally you can instantiate the controller and expect a call to have been made to that function. Sorry for the crappy answer initially
The only way I have found to test this type of scenarios is moving the method to be tested to a separate dependency, then inject it in the controller, and provide a fake in the tests instead.
Here is a very basic working example:
angular.module('test', [])
.factory('loadResponses', function() {
return function() {
//do something
}
})
.controller('aCtrl', ['$scope', 'loadResponses', function($scope, loadResponses) {
$scope.loadResponses = loadResponses;
$scope.loadResponses();
}]);
describe('test spec', function(){
var scope;
var loadResponsesInvoked = false;
var fakeLoadResponses = function () {
loadResponsesInvoked = true;
}
beforeEach(function () {
module('test', function($provide) {
$provide.value('loadResponses', fakeLoadResponses)
});
inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', { $scope: scope });
});
});
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function () {
expect(loadResponsesInvoked).toBeTruthy();
});
});
For real world code you will probably need extra work (for example, you may not always want to fake the loadResponses method), but you get the idea.
Also, here is a nice article that explains how to create fake dependencies that actually use Jasmine spies: Mocking Dependencies in AngularJS Tests
EDIT: Here is an alternative way, that uses $provide.delegate and does not replace the original method:
describe('test spec', function(){
var scope, loadResponses;
var loadResponsesInvoked = false;
beforeEach(function () {
var loadResponsesDecorator = function ($delegate) {
loadResponsesInvoked = true;
return $delegate;
}
module('test', function($provide) {
$provide.decorator('loadResponses', loadResponsesDecorator);
});
inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', { $scope: scope });
});
});
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function () {
expect(loadResponsesInvoked).toBeTruthy();
});
});
I didn't quite understand any of the answers above.
the method I often use - don't test it, instead test the output it makes..
you have not specified what loadResponses actually does.. but lets say it puts something on scope - so test existence of that..
BTW - I myself asked a similar question but on an isolated scope
angular - how to test directive with isolatedScope load?
if you still want to spy - on an unisolated scope, you could definitely use a technique..
for example, change your code to be
if ( !$scope.loadResponses ){
$scope.loadResponses = function(){}
}
$scope.loadResponses();
This way you will be able to define the spy before initializing the controller.
Another way, is like PSL suggested in the comments - move loadResponses to a service, spy on that and check it has been called.
However, as mentioned, this won't work on an isolated scope.. and so the method of testing the output of it is the only one I really recommend as it answers both scenarios.

Resources