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.
Related
I'm curious about the best way to spy on dependencies so I can make sure that their methods are being called in my services. I reduced my code to focus on the problem at hand. I'm able to test my service fine, but I want to also be able to confirm that my service (In this case metricService) has methods that are also being called. I know I have to use createSpyObj in some way, but while the function is executing properly, the spyObj methods are not being caught. Should I even be using createSpyObj? Or should I use spyObj? I'm a but confused about the concept of spying when it concerns dependencies.
UPDATE: When using SpyOn I can see one method getting called, but other methods are not
Test.spec
describe("Catalogs service", function() {
beforeEach(angular.mock.module("photonServicesCommons"));
var utilityService, metricsService, loggerService, catalogService, localStorageService;
var $httpBackend, $q, $scope;
beforeEach(
inject(function(
_catalogService_,
_metricsService_,
_$rootScope_,
_$httpBackend_
) {
catalogService = _catalogService_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', "/ctrl/catalog/all-apps").respond(
{
catalogs: catalogs2
}
);
metricsService = _metricsService_;
startScope = spyOn(metricsService, 'startScope')
emitSuccess = spyOn(metricsService, 'emitGetCatalogSuccess').and.callThrough();
endScope = spyOn(metricsService, 'endScope');
})
);
afterEach(function(){
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('get catalog', function(){
it("Should get catalogs", function(done) {
catalogService.normalizedDynamicAppList = testDynamicAppList1;
catalogService.response = null;
var promise3 = catalogService.getCatalog();
promise3.then(function (res) {
expect(res.catalogs).toEqual(catalogs2);
});
expect(metricsService.startScope).toHaveBeenCalled();
expect(metricsService.emitGetCatalogSuccess).toHaveBeenCalled();
expect(metricsService.endScope).toHaveBeenCalled();
$scope.$digest();
done();
$httpBackend.flush();
});
});
});
Service
public getCatalog(): IPromise<Interfaces.CatalogsResponse> {
if (this.response !== null) {
let finalResponse:any = angular.copy(this.response);
return this.$q.when(finalResponse);
}
return this.$q((resolve, reject) => {
this.metricsService.startScope(Constants.Photon.METRICS_GET_CATALOG_TIME);
this.$http.get(this.catalogEndpoint).then( (response) => {
let data: Interfaces.CatalogsResponse = response.data;
let catalogs = data.catalogs;
if (typeof(catalogs)) { // truthy check
catalogs.forEach((catalog: ICatalog) => {
catalog.applications.forEach((application: IPhotonApplication) => {
if( !application.appId ) {
application.appId = this.utilityService.generateUUID();
}
})
});
} else {
this.loggerService.error(this.TAG, "Got an empty catalog.");
}
this.response = data;
this.metricsService.emitGetCatalogSuccess();
console.log("CALLING END SCOPE");
this.metricsService.endScope(Constants.Photon.METRICS_GET_CATALOG_TIME);
resolve(finalResponse);
}).catch((data) => {
this.loggerService.error(this.TAG, "Error getting apps: " + data);
this.response = null;
this.metricsService.emitGetCatalogFailure();
reject(data);
});
});
} // end of getCatalog()
Instead of using createSpyObj, you can just use spyOn. As in:
beforeEach(
inject(function(
_catalogService_,
_$rootScope_,
_$httpBackend_,
_metricsService_ //get the dependecy from the injector & then spy on it's properties
) {
catalogService = _catalogService_;
metricsService = _metricsService_;
$scope = _$rootScope_.$new();
...
// create the spy object for easy referral later on
someMethodSpy = jasmine.spyOn(metricsService, "someMethodIWannaSpyOn")
})
);
describe('get catalog', function(){
it("Should get catalogs", function(done) {
catalogService.normalizedDynamicAppList = testDynamicAppList1;
catalogService.response = null;
var promise3 = catalogService.getCatalog();
...other expects
...
//do the spy-related expectations on the function spy object
$httpBackend.flush(); // this causes the $http.get() to "move forward"
// and execution moves into the .then callback of the request.
expect(someMethodSpy).toHaveBeenCalled();
});
});
I've used this pattern when testing complex angular apps, along with wrapping external imported/global dependencies in angular service wrappers to allow spying and mocking them for testing.
The reason that createSpyObject won't work here is that using it will create a completely new object called metricService with the spy props specified. It won't be the same "metricService" that is injected into the service being tested by the angular injector. You want to get the actual same singleton service object from the injector and then spy on it's properties.
The other source of dysfunction was the $httpBackend.flush()s location. The $httpBackend is a mock for the $http service: you pre-define any number of expected HTTP requests to be made by the code you are testing. Then, when you call the function that internally uses $http to make a request to some url, the $httpBackend instead intercepts the call to $http method (and can do things like validate the request payload and headers, and respond).
The $http call's then/error handlers are only called after the test code calls $httpBackend.flush(). This allows you to do any kind of setup necessary to prep some test state, and only then trigger the .then handler and continue execution of the async logic.
For me personally this same thing happens every single time I write tests with $httpBackend, and it always takes a while to figure out or remember :)
I'm having trouble getting a mocked method that returns a promise to trigger the then connected to it.
Here is the service being tested. The get method is the important aspect of it.
angular.module('service.overtimeRules').factory('OvertimeRules', OvertimeRules);
function OvertimeRules(_, Message, OvertimeRulesModel) {
const rules = [];
return { get };
function get() {
return OvertimeRulesModel.get().then(
data => {
console.log('This never fires :(');
rules.push(...data);
},
() => Message.add('Error retrieving the overtime rules.', 'danger')
);
}
}
Here is the spec file. The error I get is TypeError: undefined is not an object (evaluating 'OvertimeRulesModel.get'). If I remove the get describe wrapper, the error goes away, but the service method's then block still doesn't fire.
describe('OvertimeRules', () => {
let $q, $rootScope, deferred, Message, OvertimeRules, OvertimeRulesModel;
beforeEach(() => {
module('globals');
module('service.overtimeRules');
module($provide => {
$provide.value('Message', Message);
$provide.value('OvertimeRulesModel', OvertimeRulesModel);
});
inject((_$q_, _$rootScope_, _OvertimeRules_) => {
$q = _$q_;
$rootScope = _$rootScope_;
OvertimeRules = _OvertimeRules_;
deferred = { OvertimeRulesModel: { get: $q.defer(), update: $q.defer } };
Message = jasmine.createSpyObj('Message', ['add']);
OvertimeRulesModel = jasmine.createSpyObj('OvertimeRulesModel', ['get']);
OvertimeRulesModel.get.and.returnValue(deferred.OvertimeRulesModel.get.promise);
});
});
describe('get', () => { // why would this cause an error?
it('gets a list of rules and pushes them into the rules array', () => {
deferred.OvertimeRulesModel.get.resolve(['rules']);
OvertimeRules.get();
$rootScope.$digest(); // $apply doesn't work either
expect(OvertimeRules.rules).toEqual(['rules']);
});
});
});
Possibly important notes:
I'm happy to approach this another way as long as the code is concise and readable. Also, I need to reject promises as well.
OvertimeRulesModel is another factory in this service.overtimeRules module; Message is part of a separate module.
I tried moving OvertimeRulesModel to its own module, but got the same error.
I'm using the
A = jasmine.createSpyObj(...); A.get.and.returnValue(promise);
approach in a controller spec and everything is working fine
These are unit tests, so I'd prefer to avoid injecting the actual OvertimeRulesModel service.
Better answer
Seems like I was losing reference the variables being supplied to the module provider. Solution is just to move the createSpyObj calls above the $provide statement.
...
module('service.overtimeRules');
Message = jasmine.createSpyObj('Message', ['add']);
OvertimeRulesModel = jasmine.createSpyObj('OvertimeRulesModel', ['get']);
module($provide => {
$provide.value('Message', Message);
$provide.value('OvertimeRulesModel', OvertimeRulesModel);
});
...
Going with solution fixed another issue I ran into. Also, I didn't have to change the returnValue syntax.
Old answer:
And apparently, all I needed to do was change returnValue to callFake like so:
OvertimeRulesModel = jasmine.createSpyObj('OvertimeRulesModel', ['get']);
OvertimeRulesModel.get.and.callFake(() => deferred.OvertimeRulesModel.get.promise);
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.
I'm very new in unit testing angularjs applications and I think I don't understand the main concept of testing promise based services on angularjs.
I will directly start with my example:
I have a SQLite db-service which has this method:
var executeQuery = function(db,query,values,logMessage) {
return $cordovaSQLite.execute(db, query, values).then(function(res) {
if(res.rows.length>0) return res;
else return true;
}, function (err) {
return false;
});
};
And I want to write a test case, where I execute a query and then I want to get the return value of the executeQuery function of my service.
My test description is this:
describe("Test DatabaseCreateService‚", function () {
var DatabaseCreateService,cordovaSQLite,ionicPlatform,rootScope,q;
var db=null;
beforeEach(module("starter.services"));
beforeEach(module("ngCordova"));
beforeEach(module("ionic"));
beforeEach(inject(function (_DatabaseCreateService_, $cordovaSQLite,$ionicPlatform,$rootScope,$q) {
DatabaseCreateService = _DatabaseCreateService_;
cordovaSQLite = $cordovaSQLite;
ionicPlatform = $ionicPlatform;
q = $q;
rootScope = $rootScope;
ionicPlatform.ready(function() {
db = window.openDatabase("cgClientDB-Test.db", '1', 'my', 1024 * 1024 * 100);
});
}));
describe("Test DatabaseCreateService:createTableLocalValues", function() {
it("should check that the createTableLocalValues was called correctly and return correct data", function() {
var deferred = q.defer();
deferred.resolve(true);
spyOn(DatabaseCreateService,'createTableLocalValues').and.returnValue(deferred.promise);
var promise = DatabaseCreateService.createTableLocalValues(db);
expect(DatabaseCreateService.createTableLocalValues).toHaveBeenCalled();
expect(DatabaseCreateService.createTableLocalValues).toHaveBeenCalledWith(db);
expect(DatabaseCreateService.createTableLocalValues.calls.count()).toEqual(1);
promise.then(function(resp) {
expect(resp).not.toBe(undefined);
expect(resp).toBe(true);
},function(err) {
expect(err).not.toBe(true);
});
rootScope.$apply();
});
});
});
This test description works but it does not return the value from the function instead of it return what gets resolved in deferred.resolve(true);
What I want to do is the call the DatabaseCreateService.createTableLocalValues function and resolve the data which gets returned from the function.
The createTableLocalValues function is this:
var createTableLocalValues = function(db) {
var query = "CREATE TABLE IF NOT EXISTS `local_values` (" +
"`Key` TEXT PRIMARY KEY NOT NULL," +
"`Value` TEXT );";
return executeQuery(db,query,[],"Create cg_local_values");
};
Well if I run this method on browser or device I get a true back if everything works fine and the table gets created. So how do I get this real true also in the test description and not a fake true like in my example above?
Thanks for any kind of help.
Example 2 (with callThrough):
describe('my fancy thing', function () {
beforeEach(function() {
spyOn(DatabaseCreateService,'createTableSettings').and.callThrough();
});
it('should be extra fancy', function (done) {
var promise = DatabaseCreateService.createTableSettings(db);
rootScope.$digest();
promise.then(function(resp) {
console.log(resp);
expect(resp).toBeDefined();
expect(resp).toBe(true);
done();
},function(err) {
done();
});
});
});
Log message in karma-runner:
LOG: true
Chrome 46.0.2490 (Mac OS X 10.11.1) Test DatabaseCreateService‚ testing createTable functions: my fancy thing should be extra fancy FAILED
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
Chrome 46.0.2490 (Mac OS X 10.11.1): Executed 42 of 42 (1 FAILED) (8.453 secs / 8.03 secs)
UPDATE:
It turned out that this problem has something to do with the $cordovaSQLite.executeQuery function itself. Somehow it have no timeout on the promise and thats what the error causes. I changed the execute function of ng-cordova to this. (hoping that this change does not break anything working)
execute: function (db, query, binding) {
var q = Q.defer();
db.transaction(function (tx) {
tx.executeSql(query, binding, function (tx, result) {
q.resolve(result);
},
function (transaction, error) {
q.reject(error);
});
});
return q.promise.timeout( 5000, "The execute request took too long to respond." );
}
With that change the tests passes correctly!
You can spy on a function, and delegate to the actual implementation, using
spyOn(DatabaseCreateService,'createTableLocalValues').and.callThrough();
You might also need to call rootScope.$digest() after you call your function, so the promise will resolve.
Edit:
When testing async code, you should use the done pattern:
it('should be extra fancy', function (done) {
var promise = DatabaseCreateService.createTableSettings(db);
rootScope.$digest();
promise.then(function(resp) {
console.log(resp);
expect(resp).toBeDefined();
expect(resp).toBe(false);
expect(resp).toBe(true);
done();
});
});
A suggestion on the way you're asserting in your test:
In your test, you are calling then on your returned promise in order to make your assertions:
promise.then(function(resp) {
expect(resp).not.toBe(undefined);
expect(resp).toBe(true);
},function(err) {
expect(err).not.toBe(true);
});
Which is forcing you to add an assertion in an error function so that your test still fails if the promise doesn't resolve at all.
Try using Jasmine Promise Matchers instead. It will make your test code that easier to read and lead to clearer error messages when your tests fail. Your test would look something like this:
expect(promise).toBeResolvedWith(true);
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.