Reputation: 5866
I am learning how to use AngularJS promises and I'm having a problem writing unit tests for them. I wrote a module with a factory
that can query for an RSS feed, parse the titles from the articles, and return a promise that will resolve an array of the title strings.
Here is the module code:
angular.module('rss', [])
.factory('rssService', ['$http', '$q', '$rootScope', function($http, $q,
$rootScope) {
return function(url) {
this.getTitles = function() {
var deferred = $q.defer(),
titles = [];
$http({
method: 'GET',
url: url
}).success(function(data) {
$(data).find('title').each(function(index, item) {
var title = $(item).text();
console.log(title);
titles.push(title);
});
deferred.resolve(titles);
});
return deferred.promise;
};
};
}])
;
And here is my Jasmine unit test code:
beforeEach(module('rss'));
describe("RSS Module", function() {
var rssService, $httpBackend;
beforeEach(inject(function(_rssService_, _$httpBackend_) {
rssService = new _rssService_('/rss');
$httpBackend = _$httpBackend_;
}));
it("Can parse an RSS Feed and get the titles", function(done) {
$httpBackend.expectGET('/rss').respond(mockRSS); // mock data declared earlier
var titles = rssService.getTitles();
$httpBackend.flush();
titles.then(function(data) {
console.log("Done!");
expect(data.length).toBe(17);
done();
});
});
});
When I run the unit test, I get the following error:
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
Which is because, while my promise resolved, it did not make a call to $apply
the changes. The solution I've read about was to inject a $rootScope
and call $rootScope.$apply()
after I resolve the promise. Doing this though results in the following error
Error: [$rootScope:inprog] $digest already in progress
which is because I'm calling $rootScope.$apply()
inside of the success()
block, so a digest is already happening.
Outside of a unit test (using an actual RSS feed) the code works fine. The controller's scope handles the digest and things work as expected.
Is there any way around this?
Upvotes: 0
Views: 2613
Reputation: 27012
Your answer is right about the order of flush
and then
. The flush
should be after the callbacks have been defined.
Also, when you're using flush
from $httpBackend, or from $timeout, there is no need to use done
: the test becomes synchronous. So the test can be:
it("Can parse an RSS Feed and get the titles", function() {
$httpBackend.expectGET('/rss').respond(mockRSS); // mock data declared earlier
rssService.getTitles().then(function(data) {
expect(data.length).toBe(17);
});
$httpBackend.flush();
});
Also, I don't think directly related to your issue, but there isn't usually a need to manually create a promise when you want to post-process the result of $http
. Using the then
callback of $http
, instead of the custom success
, you can use standard promise chaining:
this.getTitles = function() {
return $http({
method: 'GET',
url: url
}).then(function(response) {
var derivedData = {length: response.data.length};
return derivedData;
});
};
You can see this at http://plnkr.co/edit/efTzOU9RYhVIgLVMdKkd .
Upvotes: 4
Reputation: 5866
The solution I found was to move my line of code that flushed the $httpBackend
to after the code to declare the then
block. What was happening was the GET request was being flushed and my promise would be declared and resolved before I had given it a then
.
The new unit test code looks like this:
it("Can parse an RSS Feed and get the titles", function(done) {
$httpBackend.expectGET('/rss').respond(mockRSS);
var titles = rssService.getTitles();
titles.then(function(data) {
console.log("Done!");
expect(data.length).toBe(17);
done();
});
$httpBackend.flush();
});
I didn't have to change any of my actual module code, but I'm currious if there is a better way to write it using chained promises. Comments or additional answers are very welcome.
Upvotes: 0