Spying on fake promises
Ran into an issue today, which took me a little bit of time to figure out.
Over time i’ve discovered that documenting experiences has a way of providing perspective, greater insight and you can help others while at it.
So here’s my experience trying to spy on a function that returns a promise
Problem
I was trying to test some code which looks something like this
$scope.loadItems = function() {
var opts = {
...
};
Service.getItems(opts)
.then(...);
};
Service.getItems
gets the items by making an http request and returns a promise.
Testing its workings is done some where else, so i just needed to mock it for my use.
A mock of the service looks like this
...
var MockService = {
getItems: function(options) {
return {
then: function() {}
}
}
};
...
Then the test for $scope.loadItems
it('should load items', function() {
...
var itemsSpy = spyOn(Service, 'getItems');
$scope.loadItems();
expect(itemsSpy).toHaveBeenCalledWith(...);
});
...
Basically, i spy on getItems
, then i call the loadItems
which should call the getItems
,
which returns a promise of which when resolved, the .then
get’s called. Finally i check if
getItems was called.
While running the tests, i got following error
TypeError: 'undefined' is not an object (evaluating 'Service.getItems(opts).then').
This would mean that getItems
returns undefined
, instead of the promise object which has got the
.then
function on it, how could this be?
Solution
Digging deep, i noticed that it becomes undefined
after the spy gets set.
What was the spy doing? Looking into how spies work, the key to spying is that the function been spied on gets monkey patched with the some code, enabling the spy to determine when the function gets called.
Now that would be the cause of my issue. The spy monkey patches my mock getItems
function so that
it no longer returns the promise, then when .then
gets called in $scope.loadItems
it can’t be found,
causing the error above.
How do we fix this?
My goal is to be able to spy on the function, but also to maintain its
original behavior (and return the promise with the expected .then
).
Jasmine allows do stuff when the function being spied on gets called. One of the things
we can do is call a fake function using .and.callFake()
.
We can now call a function to do what our original function was to do before it got spied on.
So the code for the $scope.loadItems
test becomes
it('should load items', function() {
...
var itemsSpy = spyOn(Service, 'getItems')
.and.callFake(function() {
return {
then: function() {}
}
});
$scope.loadItems();
expect(itemsSpy).toHaveBeenCalledWith(...);
});
...
and voila!
Hope this helps someone!