Unit Testing Factories with nested promises in AngularJS - angularjs

I'm building an app heavy on JavaScript. I want to unit test my app using Jasmine. At this time, I'm trying to figure out how to write some code that another developer wrote. This code is a factory with some promise action in it. The code looks like the following:
.factory('myFactory', function ($injector, someOtherFactory) {
return function(promise) {
return promise.then(null, function(res) {
if(res.result === -1) {
promise = someOtherFactory.tryAgain('key', function another() {
return $injector.get('$http')(response.config);
});
}
return promise;
});
};
})
It's like a testing turducken. I have no idea how to test this thing. Currently, I have the following setup:
describe('Factory: myFactory', function () {
it('should contain the service', inject(function (myFactory) {
expect(myFactory).toBeDefined();
}));
});
I know this isn't anything. However, I really have no idea how to test a nested promise as shown above. Any pointers?
Thanks

Related

how to write unit test for a factory method in angularJS

I have a factory method that looks like this:
createApp: function(appType, subjectType, appIndicator) {
if (appType === 'newApp') {
return Restangular.all('v1/app').post({appType: appType, appIndicator: appIndicator}).then(function(app) {
$state.go('app.initiate.new', {appId: app.id});
});
} else {
return Restangular.all('v1/app').post({appType: appType, subjectType: subjectType, appIndicator: appIndicator}).then(function(app) {
$state.go('app.initiate.old', {appId: app.id});
});
}
}
and i want to write unit tests for it...but i'm not really sure where i can start to test it. I've only really wrote unit tests for factory methods that are a lot simpler than this (like simple math functions)
i'm using karma + jasmine for testing and so far i wrote something like this which is failing.
it('should return a new application', function(done) {
application.createApp().then(function(app) {
expect(app.appType).toEqual("newApp");
done();
});
});
any tips on how to go about testing osmething like this?
What you have done is wrong. You will have to mock the factory call.
spyOn(application,'createApp').and.callFake(function() {
return {
then : function(success) {
success({id: "abc"});
}
}
});
it('should return a new application', function(done) {
spyOn($state, 'go');
application.createApp();
expect($state.go).toHaveBeenCalled();
});

What type of spy to use for testing

I know when you use spyOn you can have different forms like .and.callFake or .andCallThrough. I'm not really sure which one I need for this code I'm trying to test...
var lastPage = $cookies.get("ptLastPage");
if (typeof lastPage !== "undefined") {
$location.path(lastPage);
} else {
$location.path('/home'); //TRYING TO TEST THIS ELSE STATEMENT
}
}
Here is some of my test code:
describe('Spies on cookie.get', function() {
beforeEach(inject(function() {
spyOn(cookies, 'get').and.callFake(function() {
return undefined;
});
}));
it("should work plz", function() {
cookies.get();
expect(location.path()).toBe('/home');
expect(cookies.get).toHaveBeenCalled();
expect(cookies.get).toHaveBeenCalledWith();
});
});
I've tried a lot of different things, but I'm trying to test the else statement. Therefore I need to make cookies.get == undefined.
Everytime I try to do that though, I get this error:
Expected '' to be '/home'.
The value of location.path() never changes when cookies.get() is equal to undefined. I think I'm using the spyOn incorrectly?
Follow-up on my mock values:
beforeEach(inject(
function(_$location_, _$route_, _$rootScope_, _$cookies_) {
location = _$location_;
route = _$route_;
rootScope = _$rootScope_;
cookies = _$cookies_;
}));
Follow-up on the functions:
angular.module('buildingServicesApp', [
//data
.config(function($routeProvider) {
//stuff
.run(function($rootScope, $location, $http, $cookies)
No names on these functions, therefore how do I call the cookies.get?
Right now, you're testing that the location.path() function works as designed. I'd say you should leave that testing to the AngularJS team :). Instead, verify that the function was called correctly:
describe('Spies on cookie.get', function() {
beforeEach((function() { // removed inject here, since you're not injecting anything
spyOn(cookies, 'get').and.returnValue(undefined); // As #Thomas noted in the comments
spyOn(location, 'path');
}));
it("should work plz", function() {
// cookies.get(); replace with call to the function/code which calls cookies.get()
expect(location.path).toHaveBeenCalledWith('/home');
});
});
Note that you shouldn't be testing that your tests mock cookies.get, you should be testing that whatever function calls the first bit of code in your question is doing the right thing.

How to test a service function in Angular that returns an $http request

I have a simple service that makes an $http request
angular.module('rootApp')
.factory('projectService', ['$http', function ($http) {
return {
getProject: getProject,
getProjects: getProjects,
};
function getProject(id) {
return $http.get('/projects.json/', { 'params': { 'id': id }});
}
}]);
I'm wondering how can I test this simply and cleanly? Here's what I have so far in my test.
describe("Root App", function () {
var mockGetProjectResponse = null,
$httpBackend = null;
beforeEach(module('rootApp'));
beforeEach(inject(function (_$httpBackend_, projectService) {
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', '/projects.json/?id=1').response(mockGetProjectResponse);
}));
describe("should get projects successfully", inject(function (projectService) {
it("should return project", function () {
// I essentially want to do something like this (I know this isn't the right format).. but:
//expect(projectService.getProject(1)).toBe(mockGetProjectResponse);
});
}));
I want to avoid explicitly calling $http.get(...) in my test, and rather stick to calling the service function, i.e. projectService.getProject(1). What I'm stuck on is not being able to do something like this:
projectService.getProject(1)
.success(function (data) {
expect(data).toBe(whatever);
})
.error(function () {
});
Since there's 'no room' to call $httpBackend.flush();
Any help would be much appreciated. Thanks.
The usual recipe for testing promises (including $http) is
it("should return project", function () {
var resolve = jasmine.createSpy('resolve');
projectService.getProject(1).then(resolve);
expect(resolve).toHaveBeenCalledWith(mockGetProjectResponse);
});
A good alternative is jasmine-promise-matchers which eliminates the need for spy boilerplate code.
Here's a plunker that demonstrates both of them.
Generally the one may want to keep the methods that make $http calls as thin as possible and stub them instead, so mocking $httpBackend may not be required at all.
In current example the spec tests literally nothing and can be omitted and left to e2e tests if the code coverage isn't an end in itself.

Mocking the $injector Service

I have a situation where I want to add services inside a module, as I may not know what they are beforehand. From looking at the docs, it seems that the only way to do this (without global scope) is with Angular's $injector service. However, it seems that this service is not mockable, which makes sense as it is the way Angular itself gets the dependencies, which are still important even in testing.
Essentially, I am emulating NodeJS's passport module. I want to have something like a keychain, where you add or remove an account during runtime. So far, I have this:
angular.module('myModule').factory('accounts', function($injector) {
return {
add: function(name) {
if(!$injector.has(name) {
$log.warn('No Angular module with the name ' + name + ' exists. Aborting...');
return false;
}
else {
this.accounts[name] = $injector.get(name);
return true;
}
},
accounts: []
};
});
However, whenever I try to mock the $injector function in Jasmine, like this:
describe('accounts', {
var $injector;
var accounts;
beforeEach(function() {
$injector = {
has: jasmine.createSpy(),
get: jasmine.createSpy()
};
module(function($provide) {
$provide.value('$injector', $injector);
});
module('ngMock');
module('myModule');
inject(function(_accounts_) {
accounts = _accounts_;
});
});
describe('get an account', function() {
describe('that exists', function() {
beforeEach(function() {
$injector.has.and.returnValue(true);
});
it('should return true', function() {
expect(accounts.add('testAccount')).toEqual(true);
});
});
describe('that doesn't exist', function() {
beforeEach(function() {
$injector.has.and.returnValue(false);
});
it('should return true', function() {
expect(accounts.add('testAccount')).toEqual(false);
});
});
});
});
the 2nd test fails because the accounts service is calling the actual $injector service, and not the mock. I can confirm this by calling $injector.get or $injector.has during the test or in the service itself.
What should I do? There seems to be no other way to add new dependencies, but this is exactly what I want to do. Am I wrong? Is there in fact another way to do this, without using $injector?
Assuming I am right, and there is no other way to do what I want to do, how should I go about testing this function? I could just trust that the $injector service does its job, but I still want to mock it for the tests. I could manually add the dependencies during the inject function, but that doesn't replicate the actual behavior. I could just not test the function, but then I wouldn't be testing the function.

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.

Resources