AngularJS, spies, and resolving promises - angularjs

I am writing tests for a controller. One method calls a method in a service, which utilises a promise. In my test, I have mocked the service, and (I think) correctly mocked the promise. I have been following this blog entry: http://codingsmackdown.tv/blog/2012/12/28/mocking-promises-in-unit-tests/.
Here is the test code:
describe('Controller: ResultsController', function () {
'use strict';
var ctrl;
var ResultsServiceMock;
var RouteServiceMock;
var $scope;
var mockResults;
var mockPromise;
var q;
var deferred;
beforeEach(module('waApp'));
beforeEach(function() {
ResultsServiceMock = {
get: function(query) {
deferred = q.defer();
return deferred.promise;
}
};
RouteServiceMock = {
getParams: function() {
}
};
});
beforeEach(inject(function(
$rootScope,
$controller,
$q
) {
$scope = $rootScope.$new();
q = $q;
ctrl = $controller('ResultsController', {
$scope: $scope,
results: ResultsServiceMock,
route: RouteServiceMock
});
}));
it('Should simulate requesting results from the api', function() {
spyOn(ResultsServiceMock, 'get').andCallThrough();
spyOn(RouteServiceMock, 'getParams').andReturn({input:'hamburger'});
$scope.getResults({input:'hamburger'}); // TODO give params. try query()
deferred.resolve();
$scope.$root.$digest();
expect($scope.getResults).toHaveBeenCalled();
});
});
However, when I run the tests, I get the following error:
Chrome 35.0 (Mac) Controller: ResultsController Should simulate requesting results from the api FAILED
TypeError: Cannot read property 'resolve' of undefined
at null.<anonymous> (/Users/maryc/wa/app/results/results-controller_test.js:70:17)
Chrome 35.0 (Mac): Executed 14 of 14 (1 FAILED) (0.313 secs / 0.093 secs)
I don't understand where this error is coming from; is it because the spy call on ResultsServiceMock is not working? Any help would be appreciated.
The function getResults is as follows:
$scope.getResults = function(params) {¬
$scope.$emit('startGetResults');¬
$scope.loading = true;¬
$scope.podRequestStatus.init = false;¬
$scope.podRequestStatus = {¬
async: {}¬
};¬
var didyoumeans;¬
if(params.didyoumeans) {¬
didyoumeans = params.didyoumeans;¬
delete params.didyoumeans;¬
}¬
ResultsService.get(params).success(function(result) {¬
$scope.$emit('getResultsSuccess');¬
if(!_.isUndefined(didyoumeans)) {¬
$scope.results.queryresult.didyoumeans = didyoumeans;¬
} else if(!_.isUndefined(result.queryresult.didyoumeans)) {¬
if (!_.isArray(result.queryresult.didyoumeans)){¬
result.queryresult.didyoumeans = [result.queryresult.didyoumeans];¬
}¬
$scope.getResults({input: result.queryresult.didyoumeans[0].val, didyoumeans: result.queryresul t.didyoumeans});¬
return;¬
}¬
$scope.loading = false;¬
$scope.podRequestStatus.init = true;¬
if(result.queryresult.success === false) { //TODO is this result.results.queryresult.success??¬
if(result.queryresult.error !== false) {¬
$log.error('Results error', 'code: ' + result.queryresult.error.code, 'msg: ' + result.quer yresult.error.msg);¬
switch (result.queryresult.error.code){¬
case '3000':¬
$location.url('/blockedip');¬
break;¬
}¬
return;¬
}¬
if($scope.results.queryresult.examplepage && $scope.results.queryresult.examplepage.category) { ¬
$scope.examples();¬
}¬
// convert tips to an array if we have a single item¬
if($scope.results.queryresult.tips && !_.isArray($scope.results.queryresult.tips)){¬
$scope.results.queryresult.tips = [$scope.results.queryresult.tips];¬
}¬
$log.error('Results error');¬
return;¬
}¬
$scope.results.queryresult.pods = _.map($scope.results.queryresult.pods, function(pod) {¬
pod.priority = PodService.priority.initial;¬
return pod;¬
});¬
if ($scope.results.queryresult.sources && _.where($scope.results.queryresult.sources, {'text':'Fina ncial data'})) {¬
$scope.$emit('financialData', true);¬
} else {¬
$scope.$emit('financialData', false);¬
}¬ ¬
$scope.asyncPods(PodService.priority.async, 'async');¬
$scope.recalculate();¬
$scope.related();¬
}).error(function() {¬
$log.error('error occurred during ResultsService.get call in ResultsController');¬
});¬
};¬
The functions asyncPods, recalculate and related are three other methods within the ResultsController.
Edited: Having fixed the first error, I now get the following error when running the tests:
Chrome 35.0 (Mac) Controller: ResultsController Should simulate requesting results from the api FAILED
TypeError: undefined is not a function
at Scope.$scope.getResults (/Users/maryc/wa/.tmp_test/results-controller.js:222:36)
at null.<anonymous> (/Users/maryc/wa/app/results/results-controller_test.js:67:16)
This error comes from the line at the beginning of getResults() which calls ResultsService.get(). This seems to imply that my promise is either not being resolved, or the call $scope.getResults() is somehow failing?
The code for the .get() function of ResultsService is:
get: function(query) {¬
this.reset();¬
return ApiService.get({¬
params: UtilService.merge(query, {¬
async: true,¬
scantimeout: 1,¬
formattimeout: 8,¬
parsetimeout: 5,¬
format: 'image,plaintext,imagemap',¬
banners: 'true'¬
}),¬
timeout: abort.promise,¬
type: 'init',¬
cache: UserService.user.cacheResults¬
}).success(function(data){results.queryresult = data.queryresult;});¬
},¬
I'm wondering now if the problem is that .get itself contains a promise?

From the information you have included so far, this is my guess of what might cause the problem.
In your $scope.getResults() method, you call the get() method of ResultsService, so it seems like a name of the service is ResultsService.
But in the unit testing, you pass the ResultsServiceMock as results:
ctrl = $controller('ResultsController', {
$scope: $scope,
results: ResultsServiceMock,
route: RouteServiceMock
});
It should be ResultsService instead like this:
ctrl = $controller('ResultsController', {
$scope: $scope,
ResultsService: ResultsServiceMock,
RouteService: RouteServiceMock
});
After this change, you might encounter another problems, but it should take you pass the error:
TypeError: Cannot read property 'resolve' of undefined
Hope this helps.

Related

angularjs unit test promise based service (SQlite database service)

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);

Mocking a service used in a filter

I'm writing a unit test for a very simple filter:
app.filter('htmlsafe', ['$sce', function($sce) {
return function(message) {
return $sce.trustAsHtml(message);
};
}]);
I want to mock the $sce service and verify that the trustAsHtml method is being called. Checking the docs has not lead me to much success and after much googling this is the best I can come up with (still not working):
(function (describe, it, expect, inject, beforeEach, module) {
describe('htmlsafe filter', function () {
var htmlsafe, $sce, untrustedString;
beforeEach(module('ComstackPmApp'));
beforeEach(function() {
module(function ($provide) {
$provide.service('$sce', $sce);
});
});
beforeEach(inject(function(htmlsafeFilter) {
htmlsafe = htmlsafeFilter;
untrustedString = '<p>Untrusted</p>';
$sce = {
trustAsHtml: function() {
// stub method to spy on.
}
};
}));
it('Should mark a string as HTML safe', function () {
spyOn($sce, 'trustAsHtml');
htmlsafe(untrustedString);
expect($sce.trustAsHtml.calls.count()).toEqual(1);
});
});
})(describe, it, expect, inject, beforeEach, angular.mock.module);
However this leaves me with the following error message:
PhantomJS 1.9.8 (Mac OS X 0.0.0) htmlsafe filter Should mark a string as HTML safe FAILED
TypeError: 'undefined' is not an object (evaluating '(isArray(Type) ? Type[Type.length - 1] : Type).prototype')
undefined
at instantiate (bower_components/angular/angular.js:4480)
at bower_components/angular/angular.js:4341
at invoke (bower_components/angular/angular.js:4473)
at enforcedReturnValue (bower_components/angular/angular.js:4325)
at invoke (bower_components/angular/angular.js:4473)
at bower_components/angular/angular.js:4290
at getService (bower_components/angular/angular.js:4432)
at invoke (bower_components/angular/angular.js:4464)
at enforcedReturnValue (bower_components/angular/angular.js:4325)
at invoke (bower_components/angular/angular.js:4473)
at bower_components/angular/angular.js:4290
at getService (bower_components/angular/angular.js:4432)
at invoke (bower_components/angular/angular.js:4464)
at workFn (bower_components/angular-mocks/angular-mocks.js:2426)
Error: spyOn could not find an object to spy upon for trustAsHtml()
at specs/filters/HtmlSafeFilter.js:26
Not sure what you're trying to do with all that stuff. You don't need to provide the $sce service: Angular provides it. You don't have to create a fake one either: just spy on the angular-provided service:
describe('htmlsafe filter', function() {
var htmlsafe, $sce, untrustedString;
beforeEach(module('ComstackPmApp'));
beforeEach(inject(function(_$sce_, htmlsafeFilter) {
htmlsafe = htmlsafeFilter;
untrustedString = '<p>Untrusted</p>';
$sce = _$sce_;
}));
it('Should mark a string as HTML safe', function () {
spyOn($sce, 'trustAsHtml');
htmlsafe(untrustedString);
expect($sce.trustAsHtml.calls.count()).toEqual(1);
});
});

endpoint not found when testing using Karma, breeze, angular, mocha bardjs

I am a beginner javascript developer. My first unit test code failed and I want to resolve the problem.
The message error is Warned [web-server]: 404: /cash register/product?
ERROR: 'Error: NOT FOUND
My unit test code is :
describe("SaveCashregister:", function(done){
beforeEach(bard.asyncModule('app'));
describe("#Product", function(){
it("Add a new Product to the cashregisterDb database", function(done){
bard.inject(this, '$controller', '$log', '$q', '$rootScope', 'dataservice');
dataservice
.getProducts()
.then(function(data) {
expect(data).to.have.length(1);
})
.then(done,done);
});
});
});
The getProducts method, I want to test is :
function getProducts()
{
// http://www.breezejs.com/sites/all/apidocs/classes/EntityManager.html
var query = breeze.EntityQuery
.from('produit');
//var prodType = manager.getEntityType('Product');
var products = manager.getEntities('Produit');
return products.length ?
util.$q.when(products) :
manager.executeQuery(query)
.then(function(data){
logger.log(" codeBar: " + data.results[0].codebar);
isReady = true;
return data.results;})
.catch(queryFailed);
}
Did you try changing
var products = manager.getEntities('Produit');
to
var products = manager.getEntities('Product');
(I would have rather put a response like this into a comment but my rep isn't high enough.)
If that doesn't fix it can you provide more of the stack-trace?

AngularJS testing with firebase/mockfirebase in a service

I've been trying to write some unit tests for my services which use AngularFire to communicate with Firebase inside an Angular website.
I'm new to AngularJS and so I feel like I'm missing something obvious but couldn't find any great examples online (at least not that spoke to my limited knowledge).
I found some limited docs on MockFirebase https://github.com/katowulf/mockfirebase/tree/master/tutorials and that showed how to pretty much mock out the data so I did that.
For further examples of mockfirebase I looked at the angular fire's unit tests https://github.com/firebase/angularfire/tree/master/tests/unit but that didn't seem to show me the right way.
Here is my service --
app.service('Subscription', function ($firebase, FIREBASE_URL, $q) {
var ref;
var Subcription = {
ref: function () {
if (!ref) ref = new Firebase(FIREBASE_URL + "/subscriptions");
return ref;
},
validateSubscription: function(userId){
var defer = $q.defer();
$firebase(Subcription.ref().child(userId))
.$asObject()
.$loaded()
.then(function (subscription) {
defer.resolve(subscription.valid === true);
});
return defer.promise;
},
recordSubscription: function(userId){
return Subcription.ref().$set(userId, {valid: true});
}
};
return Subcription;
});
Here is the spec file --
describe('Service: subscription', function () {
// load the service's module
beforeEach(module('clientApp'));
// instantiate service
var subscription;
var scope;
beforeEach(inject(function (_Subscription_, $rootScope) {
MockFirebase.override();
subscription = _Subscription_;
scope = $rootScope.$new();
}));
it('allows access when the user id is in the subscription list', function () {
subscription.ref().push({'fakeUser': {valid: true}});
subscription.ref().flush();
var handler = jasmine.createSpy('success');
subscription.validateSubscription('fakeUser').then(handler);
scope.$digest();
expect(handler).toHaveBeenCalledWith(true);
});
});
It seems like the problem is that the promise never gets resolved inside of $asobject.$loaded because that angularfire part isn't happening.
I get the following as a result of the test: 'Expected spy success to have been called with [ true ] but it was never called.'

Not able to write Jasmine test case for Controller in ANgularJS

I have following code in my controller and I want to write Jasmine test case for this part.
I tried to write one, but its throwing following error
TypeError: Object [object Array] has no method 'then'
Controller Code ::
$scope.doGetList = function() {
var queryString = {......sending some query parameters};
searchResource.getList(queryString).then(
function (data) {
$scope.sugesstions = data;
}
);
};
Jasmine Test Case ::
it("should return provided list", angular.mock.inject(function($rootScope, $controller) {
var scope = $rootScope.$new();
var searchResource = {
getList: function() {
return ['suggestions1', 'suggestions2', 'suggestions3'];
}
};
$controller(
headerController,
{
$scope: scope,
cartsService: null,
currentUser: null,
searchResource: searchResource
}
);
expect(scope.sugesstions).toBeDefined();
expect(scope.sugesstions.length).toBe(0);
//this method will call mock method instead of actual server call
scope.doGetAutocomplete();
expect(scope.sugesstions.length).toBe(3);
expect(scope.sugesstions[0]).toEqual('suggestions1');
expect(scope.sugesstions[1]).toEqual('suggestions2');
expect(scope.sugesstions[2]).toEqual('suggestions3');
}));
How should I write it.
You'd have to wrap async call in runs(). From jasmine doc: http://pivotal.github.io/jasmine/#section-Asynchronous_Support
Or, I use jasmine-as-promised with better support: https://github.com/ThomasBurleson/jasmine-as-promised

Resources