How to test an AngularJS watch using debounce function with Jasmine - angularjs

I have a controller with a watch that uses debounce from lodash to delay filtering a list by 500ms.
$scope.$watch('filter.keywords', _.debounce(function () {
$scope.$apply(function () {
$scope.filtered = _.where(list, filter);
});
}, 500));
I am trying to write a Jasmine test that simulates entering filter keywords that are not found followed by keywords that are found.
My initial attempt was to use $digest after assigning a new value to keywords, which I assume didn't work because of the debounce.
it('should filter list by reference', function () {
expect(scope.filtered).toContain(item);
scope.filter.keywords = 'rubbish';
scope.$digest();
expect(scope.filtered).not.toContain(item);
scope.filter.keywords = 'test';
scope.$digest();
expect(scope.filtered).toContain(item);
});
So I tried using $timeout, but that doesn't work either.
it('should filter list by reference', function () {
expect(scope.filtered).toContain(item);
$timeout(function() {
scope.filter.keywords = 'rubbish';
});
$timeout.flush();
expect(scope.filtered).not.toContain(item);
$timeout(function() {
scope.filter.keywords = 'test';
});
$timeout.flush();
expect(scope.filtered).toContain(item);
});
I have also tried giving $timeout a value greater than the 500ms set on debounce.
How have others solved this problem?
EDIT: I've found a solution which was to wrap the expectation in a $timeout function then call $apply on the scope.
it('should filter list by reference', function () {
expect(scope.filtered).toContain(item);
scope.filter.keywords = 'rubbish';
$timeout(function() {
expect(scope.filtered).not.toContain(item);
});
scope.$apply();
scope.filter.keywords = 'test';
$timeout(function() {
expect(scope.filtered).toContain(item);
});
scope.$apply();
});
I'm still interested to know whether this approach is best though.

This is a bad approach. You should use an angular-specific debounce such as this that uses $timeout instead of setTimeout. That way, you can do
$timeout.flush();
expect(scope.filtered).toContain(item);
and the spec will pass as expected.

I've used this:
beforeEach(function() {
...
spyOn(_, 'debounce').and.callFake(function (fn) {
return function () {
//stack the function (fn) code out of the current thread execution
//this would prevent $apply to be invoked inside the $digest
$timeout(fn);
};
});
});
function digest() {
//let the $watch be invoked
scope.$digest();
//now run the debounced function
$timeout.flush();
}
it('the test', function() {
scope.filter.keywords = ...;
digest();
expect(...);
});
Hope it helps

Using the spyOn to replace the _.debounce, check this link. http://gotoanswer.stanford.edu/?q=Jasmine+test+does+not+see+AngularJS+module

Related

Jasmine testing for function call

I'm new to jasmine testing. How can I test function call in watch function?
Below is my code. I'm confused about usage of spy in jasmine and how can I handle function call inside watcher.
Do I need to pause fetch() inside watch. Please suggest how to improve my testing skills.
var app = angular.module('instantsearch',[]);
app.controller('instantSearchCtrl',function($scope,$http,$sce){
$scope.$sce=$sce;
$scope.$watch('search', function() {
fetch();
});
$scope.search = "How create an array";
var result = {};
function fetch() {
$http.get("https://api.stackexchange.com/2.2/search?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!4*Zo7ZC5C2H6BJxWq&key=DIoPmtUvEkXKjWdZB*d1nw((")
.then(function(response) {
$scope.items = response.data.items;
$scope.answers={};
angular.forEach($scope.items, function(value, key) {
var ques = value.question_id;
$http.get("https://api.stackexchange.com/2.2/questions/"+value.question_id+"/answers?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!9YdnSMKKT&key=DIoPmtUvEkXKjWdZB*d1nw((").then(function(response2) {
$scope.answers[ques]=response2.data.items;
//console.log(JSON.stringify($scope.answers));
});
});
});
}
});
my test case:
describe('instantSearchCtrl', function() {
beforeEach(module('instantsearch'));
var $scope, ctrl;
beforeEach( inject(function($rootScope, $controller) {
// create a scope object for us to use.
$scope = $rootScope.$new();
ctrl = $controller('instantSearchCtrl', {
$scope: $scope
});
}));
/*var $scope = {};
var controller = $controller('instantSearchCtrl', { $scope: $scope });
expect($scope.search).toEqual('How create an array');
//expect($scope.strength).toEqual('strong');*/
it('should update baz when bar is changed', function (){
//$apply the change to trigger the $watch.
$scope.$apply();
//fetch().toHaveBeenCalled();
fetch();
it(" http ", function(){
//scope = $rootScope.$new();
var httpBackend;
httpBackend = $httpBackend;
httpBackend.when("GET", "https://api.stackexchange.com/2.2/search?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!4*Zo7ZC5C2H6BJxWq&key=DIoPmtUvEkXKjWdZB*d1nw((").respond([{}, {}, {}]);
});
});
});
First you should trigger the watch. For that you should change search value and after that manually run: $scope.$digest() or $scope.$apply()
In order to fully test the fetch function you should also mock the response to the second request (or the mock for all the second level requests if you want to test the iteration).
After that you should add some expect statements. For the controller code they should be:
expect($scope.items).toEqual(mockResponseToFirstRequest);
expect($scope.answers).toEqual(mockResponseCombinedForSecondLevelRequests);
As for using the spy in karma-jasmine tests, those limit the amount of the code tested. A plausible use for spy in this case is to replace the httpBackend.when with spyOn($http, 'get').and.callFake(function () {})
Here is the documentation for using spies https://jasmine.github.io/2.0/introduction.html#section-Spies

Jasmine SpyOn on $window.print is not working properly

I have a one test case which aims to check whether $window.print() is calling or not?
For that i have a written a following test case:
beforeEach(inject(function($window) {
Objwindow = $window;
}
it('Test for print', function() {
spyOn( Objwindow, 'print' ).and.callFake( function() {
console.log("Spy is called");
return true;
});
scope.printConfirmation();
expect(Objwindow.print).toHaveBeenCalled();
});
In Controller:
scope.printConfirmation = function() {
$window.print()
}
Now, If i run the above only testcase , It is running successfully without any errors. i.e.. Spyon is getting called.
But if i run the test cases of all modules(almost there are 1325 test cases), it is throwing the following error.
Expected spy print to have been called.
What might be the cause for this issue? Am i doing any thing wrong?
you have to take the instance of controller
like var controller = $controller('Controller', { $window: Objwindow });
and your test case should be inside it
it('Test for print', function() {
spyOn( Objwindow, 'print' ).and.callFake( function() {
console.log("Spy is called");
return true;
scope.printConfirmation();
expect(Objwindow.print).toHaveBeenCalled();
});

Testing $interval in Jasmine/ Karma

I have a simple factory
angular.module('myApp.dice',[]).factory('Dice', ['$interval', function($interval){
return {
rollDice: function(){
return $interval(function(c){
count++;
}, 100, 18, false, 0);
}
};
}]);
In my test case I have
describe('rolling dice', function(){
var promise, promiseCalled = false, notify = false;
beforeEach(inject(function(){
promise = Dice.rollDice();
promise.then(function(){
promiseCalled = true;
});
}));
it('should invoke a promise', inject(function(){
expect(promise).toBeDefined();
}));
it('should set promiseCalled', inject(function($rootScope, $interval){
expect(promiseCalled).toEqual(true);
}));
});
How do I trigger the interval or resolve the promise? to get the test to be true?
See plunker
You need to use '$interval.flush' from angularjs-mocks and then test for the result. I've taken the liberty to assign count to the dice object because it's undefined in your code sample.
Is there any reason why $interval is called with invokeApply = false? because then you have to manually call $apply.
The test case would be:
describe('rolling dice', function(){
var $interval, $rootScope, dice;
beforeEach(module('plunker'));
beforeEach(inject(function(_$interval_, _$rootScope_, _Dice_){
$interval = _$interval_;
$rootScope = _$rootScope_;
dice = _Dice_;
}));
it('should increment count', inject(function(){
dice.rollDice();
// Move forward by 100 milliseconds
$interval.flush(100);
// Need to call apply because $interval was called with invokeApply = false
$rootScope.$apply();
expect(dice.count).toBe(1);
}));
});
with factory:
app.factory('Dice', ['$interval', function($interval){
var self= {
count: 0,
rollDice: function(){
return $interval(function() {
self.count++;
}, 100, 18, false, 0);
}
};
return self;
}]);
EDIT:
I prefer testing the result of a function call but perhaps you have a use case for testing that a promise function is called so this might be what you're looking for. From the angular docs on $interval it says it returns
A promise which will be notified on each iteration.
Keyword being notified. And from the promise API the arguments for then are
then(successCallback, errorCallback, notifyCallback)
i.e. the third call back function notifyCallback is called for each iteration of $interval. So the test would look something like this:
it('should set promiseCalled', function(){
var promiseCalled = false;
dice.rollDice().then(null, null, function() {
promiseCalled = true;
});
$interval.flush(100);
$rootScope.$apply();
expect(promiseCalled).toBe(true);
});
If you want to test for a resolved promise, then it would look something like this:
it('should resolve promise', function(){
var promiseCalled = false;
var resolvedData = null;
$q.when('Some Data').then(function(data) {
promiseCalled = true;
resolvedData = data;
});
$rootScope.$apply();
expect(promiseCalled).toBe(true);
expect(resolvedData).toBe('Some Data');
});
I've updated the plunker with these test cases.

Testing an asynchronous function in an angular factory

Im new to testing and im trying to test my angular code in Jasmine. Im stuck on the problem of testing the answer from an resolved promise. Right now the test gets timed out. I would like to have the test waiting for the respons instead of just put in a mockup respons. How do i do that? Is it a bad way of making unit-tests?
angular.module("Module1", ['ng']).factory("Factory1", function($q){
function fn1(){
var deferred = $q.defer();
setTimeout(function(){ deferred.resolve(11); }, 100); // this is representing an async action
return deferred.promise;
}
return { func1 : fn1 };
});
describe('test promise from factory', function() {
var factory1, $rootScope, $q;
beforeEach(module('Module1'));
beforeEach(inject(function(Factory1, _$rootScope_, _$q_) {
factory1=Factory1;
$rootScope = _$rootScope_;
$q = _$q_;
}));
it('should be get the value from the resolved promise', function(done) {
factory1.func1().then(function(res){
expect(res).toBe(11);
done(); // test is over
});
$rootScope.$digest();
});
});
The setTimeout() block represents an async function call, and i dont want to replace it with something like $timeout.
I don't know why you wouldn't want to use the $timeout service.
But if you really want to use the setTimeout, a $rootScope.$digest() is required inside the callback.
function fn1() {
var deferred = $q.defer();
// this is representing an async action
setTimeout(function() {
deferred.resolve(11);
$rootScope.$digest(); // this is required ($timeout do this automatically).
}, 100);
return deferred.promise;
}

How do I test a decorator which is a wrapper around $timeout?

I have the following decorator which wraps around the original $timeout from within $rootScope. When used inside controllers, it will help by canceling the $timeout promise when the scope is destroyed.
angular.module('MyApp').config(['$provide', function ($provide) {
$provide.decorator('$rootScope', ['$delegate', function ($delegate) {
Object.defineProperty($delegate.constructor.prototype, 'timeout', {
value: function (fn, number, invokeApply) {
var $timeout = angular.injector(['ng']).get('$timeout'),
promise;
promise = $timeout(fn, number, invokeApply);
this.$on('$destroy', function () {
$timeout.cancel(promise);
});
},
enumerable: false
});
return $delegate;
}]);
}]);
But how do I properly unit test this? I kinda see 2 tests I should be doing here... 1) Check if the original $timeout was called when $rootScope.timeout() is called and 2) check if the promise is cancelled when the scope is destroyed.
Here's my current test suite for this:
describe('MyApp', function () {
var $rootScope;
beforeEach(function () {
module('MyApp');
inject(['$rootScope', function (_$rootScope_) {
$rootScope = _$rootScope_;
}]);
});
describe('$timeout', function () {
it('<something>', function () {
$rootScope.timeout(function () {}, 2500);
// Test if the real $timeout was called with above parameters
$rootScope.$destroy();
// Test if the $timeout promise was destroyed
});
});
});
The only thing that this does is giving me 100% coverage. But that's not what I want... How do I properly test this?
Since nobody was able to help me and I really wanted to have this done, I eventually found my own solution. I'm not sure it is the best solution but I think it does the trick.
Here's how I solved it:
describe('MyApp', function () {
var $rootScope,
$timeout,
deferred;
beforeEach(function () {
module('MyApp');
inject(['$rootScope', '$q', function (_$rootScope_, _$q_) {
$rootScope = _$rootScope_;
deferred = _$q_.defer();
}]);
$timeout = jasmine.createSpy('$timeout', {
cancel: jasmine.createSpy('$timeout.cancel')
}).and.returnValue(deferred.promise);
spyOn(angular, 'injector').and.returnValue({
get: function () {
return $timeout;
}
});
});
describe('$timeout', function () {
it('should set the timeout with the specified arguments', function () {
$rootScope.timeout(angular.noop, 250, false);
expect($timeout).toHaveBeenCalledWith(angular.noop, 250, false);
});
it('should cancel the timeout on scope destroy event', function () {
$rootScope.timeout(angular.noop, 250, false);
$rootScope.$destroy();
expect($timeout.cancel).toHaveBeenCalledWith(deferred.promise);
});
});
});

Resources