Angularjs Mocha test $q promises without $rootScope.$apply - angularjs

I have this service:
angular.module('domeeApp')
.factory('streamWidget', streamWidgetFactory);
function streamWidgetFactory($q) {
return {
loadContent: function() {
return $q(function(resolve, reject) {
resolve('test');
})
}
}
}
I'm testing it with karma/mocha/chai:
describe('streamWidget', function() {
beforeEach(module('domeeApp'));
var streamWidget;
var $timeout;
beforeEach(inject(function(_$timeout_, _streamWidget_) {
streamWidget = _streamWidget_;
$timeout = _$timeout_;
}));
it('should load new content', function(done) {
streamWidget.loadContent()
.then(function(res) {
expect(res).to.equal('test');
done();
})
.catch(function(){})
$timeout.flush();
});
});
Since $q promises doesn't work well with mocha i'm following this answer, which says to add $timeout.flush() to force the .then method of the promise to be executed.
The problem is, after calling .flush(), all my app wakes up and i start to get this errors from angular-mocks:
Error: Unexpected request: GET /partials/page/view/index.
I know about $httpBackend, but it would be insane to mock ALL the requests my app is making on startup.
Is there a way to make $q promises work with mocha without calling $timeout.flush() or $rootScope.$apply()?

As shown here, chai-as-promised can be used to assert $q promises.
With this setup
chaiAsPromised.transferPromiseness = function (assertion, promise) {
assertion.then = promise.then.bind(promise);
if (!('$$state' in promise))
return;
inject(function ($rootScope) {
if (!$rootScope.$$phase)
$rootScope.$digest();
});
};
digest cycles will be triggered automatically on promise assertions, executing the whole promise chain.
In this case the spec
it('...', () => {
...
expect(...).to.eventually...;
expect(...).to.eventually...;
$rootScope.$digest();
});
can omit $digest() call and become
it('...', () => {
...
expect(...).to.eventually...;
expect(...).to.eventually...;
});
Notice that $q promises are synchronous, they shouldn't be returned from Mocha spec or call done callback.

Here's an alternative strategy that we use because we never actually need $httpBackend, but it sometimes (randomly) fails making requests for templates used by directives (even though those templates are available in $templateCache):
beforeEach(function() {
module('app', function($provide) {
// This is using jasmine, but the idea is the same with mocha.
// Basically just replace $httpBackend with a function that does nothing.
$provide.constant('$httpBackend', jasmine.createSpy('$httpBackend'));
});
});
Of course, if you actually use $httpBackend in other cases, then this won't work, as you'll need it to mock response objects.

Related

Finally clause of promise never getting executed in Jasmine test

I have this jasmine test, and the finally clause on a promise appears to not be getting executed, as I get the error:
PhantomJS 2.1.1 (Mac OS X 0.0.0) Service: petsFactory .getPetsAsync() should return a list of pets FAILED
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
My test file looks like:
'use strict';
describe('Service: petsFactory', function () {
// load the service's module
beforeEach(module('smokeTestApp'));
// instantiate service
var petsFactory;
beforeEach(inject(function (_petsFactory_) {
petsFactory = _petsFactory_;
}));
describe('.getPetsAsync()', function () {
it('should return a list of pets', function (done) {
var testPets = function (pets) {
expect(Array.isArray(pets)).toBe(true);
}
var failTest = function(error) {
expect(error).toBeUndefined();
};
petsFactory
.getPetsAsync()
.then(testPets)
.catch(failTest)
.finally(done);
});
});
});
The relevant factory method looks like:
var getPetsAsync = function () {
return $q.when(pets);
};
The contents of the pets variable is totally synchronous, the promise is just a wrapper on a synchronous value that is there immediately.
What is going wrong here?
From the documentation:
When testing promises, it's important to know that the resolution of
promises is tied to the digest cycle. That means a promise's then,
catch and finally callback functions are only called after a digest
has run. In tests, you can trigger a digest by calling a scope's
$apply function. If you don't have a scope in your test, you can
inject the $rootScope and call $apply on it. There is also an example
of testing promises in the $q service documentation.
So simply inject $rootScope and use $apply:
petsFactory
.getPetsAsync()
.then(testPets)
.catch(failTest)
.finally(done);
$rootScope.$apply();

Promise resolves too late in unit test

Unit testing an app using Firebase and angularFire. Mocking Firebase with mockfirebase.
In this test, the promise is resolved after the test is finished:
describe('the service api', function() {
var promiseResolved;
beforeEach(function() {
// Inject with expected values
_setup();
promiseResolved = jasmine.createSpy('promiseResolved');
});
it('should resolve to a obj', function() {
var obj = objService.getObjFromRefString('1234/q1w2');
obj.$loaded().then(promiseResolved);
obj.$ref().flush();
expect(promiseResolved).toHaveBeenCalled(); // fails
});
});
This approach seems to work for the angularFire tests - see line 125.
If i use Jasmine async done feature:
it('should resolve to a obj', function(done) {
var obj = objService.getObjFromRefString('1234/q1w2');
obj.$ref().flush();
obj.$loaded().then(function() {
console.log('resolved');
promiseResolved();
expect(promiseResolved).toHaveBeenCalled();
done();
});
});
It fails with message "Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL".
The console.log is shown before.
The promise seems to resolve just after the timeout. I tried calling $rootScope.$apply() with no changes.
How can i make the promise resolve right away so the test can pass?
This solved it:
$timeout.flush();
Updated test:
it('should resolve to a obj', function() {
var obj = objService.getObjFromRefString('1234/q1w2');
obj.$loaded().then(promiseResolved);
obj.$ref().flush();
$timeout.flush();
expect(promiseResolved).toHaveBeenCalled(); // great success
});

How to test an error response in angular's $httpBackend?

We are using angular 1.2.x (we have to due to IE8). We are testing with Karma and Jasmine. I want to test the behavior of my modules, in case the server responds with an error. According to the angular documentation, I should just simply prepare the $httpBackend mock like this (exactly as I'd expect):
authRequestHandler = $httpBackend.when('GET', '/auth.py');
// Notice how you can change the response even after it was set
authRequestHandler.respond(401, '');
This is what I am doing in my test:
beforeEach(inject(function($injector) {
keepSessionAliveService = $injector.get('keepSessionAliveService');
$httpBackend = $injector.get('$httpBackend');
$interval = $injector.get('$interval');
}));
(...)
describe('rejected keep alive request', function() {
beforeEach(function() {
spyOn(authStorageMock, 'get');
spyOn(authStorageMock, 'set');
$httpBackend.when('POST', keepAliveUrl).respond(500, '');
keepSessionAliveService.start('sessionId');
$interval.flush(90*60*1001);
$httpBackend.flush();
});
it('should not add the session id to the storage', function() {
expect(authStorageMock.set).not.toHaveBeenCalled();
});
});
But the test fails, because the mock function is being called and I can see in the code coverage that it never runs into the error function I pass to the §promise.then as second argument.
Apparently I am doing something wrong here. Could it have to with the older angular version we're using?
Any help would be appreciated!
Something like this:
it("should receive an Ajax error", function() {
spyOn($, "ajax").andCallFake(function(e) {
e.error({});
});
var callbacks = {
displayErrorMessage : jasmine.createSpy()
};
sendRequest(callbacks, configuration);
expect(callbacks.displayErrorMessage).toHaveBeenCalled();

chai-as-promised erroneously passes tests

I'm trying to write tests for a method that returns an angular promise ($q library).
I'm at a loss. I'm running tests using Karma, and I need to figure out how to confirm that the AccountSearchResult.validate() function returns a promise, confirm whether the promise was rejected or not, and inspect the object that is returned with the promise.
For example, the method being tested has the following (simplified):
.factory('AccountSearchResult', ['$q',
function($q) {
return {
validate: function(result) {
if (!result.accountFound) {
return $q.reject({
message: "That account or userID was not found"
});
}
else {
return $q.when(result);
}
}
};
}]);
I thought I could write a test like this:
it("it should return an object with a message property", function () {
promise = AccountSearchResult.validate({accountFound:false});
expect(promise).to.eventually.have.property("message"); // PASSES
});
That passes, but so does this (erroneously):
it("it should return an object with a message property", function () {
promise = AccountSearchResult.validate({accountFound:false});
expect(promise).to.eventually.have.property("I_DONT_EXIST"); // PASSES, should fail
});
I am trying to use the chai-as-promised 'eventually', but all my tests pass with false positives:
it("it should return an object", function () {
promise = AccountSearchResult.validate();
expect(promise).to.eventually.be.an('astronaut');
});
will pass. In looking at docs and SO questions, I have seen examples such as:
expect(promise).to.eventually.to.equal('something');
return promise.should.eventually.equal('something');
expect(promise).to.eventually.to.equal('something', "some message about expectation.");
expect(promise).to.eventually.to.equal('something').notify(done);
return assert.becomes(promise, "something", "message about assertion");
wrapping expectation in runs() block
wrapping expectation in setTimeout()
Using .should gives me Cannot read property 'eventually' of undefined. What am I missing?
#runTarm 's suggestions were both spot on, as it turns out. I believe that the root of the issue is that angular's $q library is tied up with angular's $digest cycle. So while calling $apply works, I believe that the reason it works is because $apply ends up calling $digest anyway. Typically I've thought of $apply() as a way to let angular know about something happening outside its world, and it didn't occur to me that in the context of testing, resolving a $q promise's .then()/.catch() might need to be pushed along before running the expectation, since $q is baked into angular directly. Alas.
I was able to get it working in 3 different ways, one with runs() blocks (and $digest/$apply), and 2 without runs() blocks (and $digest/$apply).
Providing an entire test is probably overkill, but in looking for the answer to this I found myself wishing people had posted how they injected / stubbed / setup services, and different expect syntaxes, so I'll post my entire test.
describe("AppAccountSearchService", function () {
var expect = chai.expect;
var $q,
authorization,
AccountSearchResult,
result,
promise,
authObj,
reasonObj,
$rootScope,
message;
beforeEach(module(
'authorization.services', // a dependency service I need to stub out
'app.account.search.services' // the service module I'm testing
));
beforeEach(inject(function (_$q_, _$rootScope_) {
$q = _$q_; // native angular service
$rootScope = _$rootScope_; // native angular service
}));
beforeEach(inject(function ($injector) {
// found in authorization.services
authObj = $injector.get('authObj');
authorization = $injector.get('authorization');
// found in app.account.search.services
AccountSearchResult = $injector.get('AccountSearchResult');
}));
// authObj set up
beforeEach(inject(function($injector) {
authObj.empAccess = false; // mocking out a specific value on this object
}));
// set up spies/stubs
beforeEach(function () {
sinon.stub(authorization, "isEmployeeAccount").returns(true);
});
describe("AccountSearchResult", function () {
describe("validate", function () {
describe("when the service says the account was not found", function() {
beforeEach(function () {
result = {
accountFound: false,
accountId: null
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
message = "PROMISE REJECTED";
reasonObj = arg;
});
// USING APPLY... this was the 'magic' I needed
$rootScope.$apply();
});
it("should return an object", function () {
expect(reasonObj).to.be.an.object;
});
it("should have entered the 'catch' function", function () {
expect(message).to.equal("PROMISE REJECTED");
});
it("should return an object with a message property", function () {
expect(reasonObj).to.have.property("message");
});
// other tests...
});
describe("when the account ID was falsey", function() {
// example of using runs() blocks.
//Note that the first runs() content could be done in a beforeEach(), like above
it("should not have entered the 'then' function", function () {
// executes everything in this block first.
// $rootScope.apply() pushes promise resolution to the .then/.catch functions
runs(function() {
result = {
accountFound: true,
accountId: null
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
reasonObj = arg;
message = "PROMISE REJECTED";
});
$rootScope.$apply();
});
// now that reasonObj has been populated in prior runs() bock, we can test it in this runs() block.
runs(function() {
expect(reasonObj).to.not.equal("PROMISE RESOLVED");
});
});
// more tests.....
});
describe("when the account is an employee account", function() {
describe("and the user does not have EmployeeAccess", function() {
beforeEach(function () {
result = {
accountFound: true,
accountId: "160515151"
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
message = "PROMISE REJECTED";
reasonObj = arg;
});
// digest also works
$rootScope.$digest();
});
it("should return an object", function () {
expect(reasonObj).to.be.an.object;
});
// more tests ...
});
});
});
});
});
Now that I know the fix, it is obvious from reading the $q docs under the testing section, where it specifically says to call $rootScope.apply(). Since I was able to get it working with both $apply() and $digest(), I suspect that $digest is really what needs to be called, but in keeping with the docs, $apply() is probably 'best practice'.
Decent breakdown on $apply vs $digest.
Finally, the only mystery remaining to me is why the tests were passing by default. I know I was getting to the expectations (they were being run). So why would expect(promise).to.eventually.be.an('astronaut'); succeed? /shrug
Hope that helps. Thanks for the push in the right direction.

AngularJS $timeout function not executing in my Jasmine specs

I'm trying to test my AngularJS controller with Jasmine, using Karma. But a $timeout which works well in real-life, crashes my tests.
Controller:
var Ctrl = function($scope, $timeout) {
$scope.doStuff = function() {
$timeout(function() {
$scope.stuffDone = true;
}, 250);
};
};
Jasmine it block (where $scope and controller have been properly initialized):
it('should do stuff', function() {
runs(function() {
$scope.doStuff();
});
waitsFor(function() {
return $scope.stuffDone;
}, 'Stuff should be done', 750);
runs(function() {
expect($scope.stuffDone).toBeTruthy();
});
});
When I run my app in browser, $timeout function will be executed and $scope.stuffDone will be true. But in my tests, $timeout does nothing, the function is never executed and Jasmine reports error after timing out 750 ms. What could possibly be wrong here?
According to the Angular JS documentation for $timeout, you can use $timeout.flush() to synchronously flush the queue of deferred functions.
Try updating your test to this:
it('should do stuff', function() {
expect($scope.stuffDone).toBeFalsy();
$scope.doStuff();
expect($scope.stuffDone).toBeFalsy();
$timeout.flush();
expect($scope.stuffDone).toBeTruthy();
});
Here is a plunker showing both your original test failing and the new test passing.
As noted in one of the comments, Jasmine setTimeout mock is not being used because angular's JS mock $timeout service is used instead. Personally, I'd rather use Jasmine's because its mocking method lets me test the length of the timeout. You can effectively circumvent it with a simple provider in your unit test:
module(function($provide) {
$provide.constant('$timeout', setTimeout);
});
Note: if you go this route, be sure to call $scope.apply() after jasmine.Clock.tick.
As $timeout is just a wrapper for window.setTimeout, you can use jasmines Clock.useMock() which mocks the window.setTimeout
beforeEach(function() {
jasmine.Clock.useMock();
});
it('should do stuff', function() {
$scope.doStuff();
jasmine.Clock.tick(251);
expect($scope.stuffDone).toBeTruthy();
});

Resources