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);
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 looking to write a Jasmine unit test which executes a callback function passed to a then function. This then function is chained to a call to the AngularJS $http service, and it's inside a custom service. Here's the code I'm working with:
app.service('myService', function($rootScope, $http) {
var service = this;
var url = 'http://api.example.com/api/v1/resources';
service.resources = {
current: []
};
service.insertResource = function (resource) {
return $http.post(url, resource).then(function(response){
$rootScope.$broadcast('resources:updated', service.resources.current);
return response;
});
};
});
Here's my attempt to write a test which executes this callback, but to no avail:
describe('resource service', function() {
beforeEach(angular.mock.module('myapp'));
var resourceService;
beforeEach(inject(function(_resourceService_) {
resourceService = _resourceService_;
}));
it('should insert resources', function() {
resourceService.insertResource({});
});
});
There are several approaches you could take:
Use $httpBackend.expectPOST
Use $httpBackend.whenPOST
Move the code in the callback to a named function (not an anonymous one) and write a test for this function. I sometimes take this route b/c I don't want the trouble of writing tests with $httpBackend. I only test the callback function, I don't test that my service is calling the callback. If you can live w/that it's much simpler approach.
Check the documentation for $httpBackend for details. Here's a simple example:
describe('resource service', function() {
beforeEach(angular.mock.module('myapp'));
var resourceService, $httpBackend;
beforeEach(inject(function($injector) {
resourceService = $injector.get('resourceService');
$httpBackend = $injector.get('$httpBackend');
}));
afterEach(function() {
// tests will fail if expected HTTP requests are not made
$httpBackend.verifyNoOutstandingRequests();
// tests will fail if any unexpected HTTP requests are made
$httpBackened.verifyNoOutstandingExpectations();
});
it('should insert resources', function() {
var data: { foo: 1 }; // whatever you are posting
// make an assertion that you expect this POST to happen
// the response can be an object or even a numeric HTTP status code (or both)
$httpBackend.expectPOST('http://api.example.com/api/v1/resources', data).respond({});
// trigger the POST
resourceService.insertResource({});
// This causes $httpBackend to trigger the success/failure callback
// It's how you workaround the asynchronous nature of HTTP requests
// in a synchronous way
$httpBackend.flush();
// now do something to confirm the resource was inserted by the callback
});
});
NB: Code reproduced from memory.
I have a method generated by djangoAngular that has this signature in my service:
angular.module('myModule')
.service('PythonDataService',['djangoRMI',function(djangoRMI){
return {getData:getData};
function getData(foo,bar,callback){
var in_data = {'baz':foo,'bing':bar};
djangoRMI.getPythonData(in_data)
.success(function(out_data) {
if(out_data['var1']){
callback(out_data['var1']);
}else if(out_data['var2']){
callback(out_data['var2']);
}
}).error(function(e){
console.log(e)
});
};
}])
I want to test my service in Jasmine, and so I have to mock my djangoAngular method. I want to call through and have it return multiple datum.
This is (sort of) what I have tried so far, reproduced from memory:
describe('Python Data Service',function(){
var mockDjangoRMI,
beforeEach(module('ng.django.rmi'));
beforeEach(function() {
mockDjangoRMI = {
getPythonData:jasmine.createSpy('getPythonData').and.returnValue({
success:function(fn){fn(mockData);return this.error},
error:function(fn){fn();return}
})
}
module(function($provide) {
$provide.provide('djangoRMI', mockDjangoRMI);
});
});
it('should get the data',function(){
mockData = {'var1':'Hello Stackexchange'};
var callback = jasmine.createSpy();
PythonDataService.getData(1,2,callback);
expect(callback).toHaveBeenCalled();
})
})
But when I put another it block in with a different value for mockData, only one of them is picked up.
I'm guessing that because of the order of operation something is not right with how I'm assigning mockData. How can I mock multiple datum into my djangoRMI function?
I have to write a test-case using karma to the following code.
$scope.getUserDetails = function (param) {
Api.getData(param).then(function (result) {
Api.getVal(result).then(function (data) {
var display = Api.userDetails(result.id, result.name);
$scope.username = display.name;
});
});
};
But i am facing the problem because of test case fails due to missing parameter from call-back function. I tried many methods. But test-cases failed due to result.id is undefined. Following is just a scaffold of my test-case. 'apiCall' are defined in beforeEach.
it('should test the getData()', function () {
var user = 'John';
scope.getUserDetails(123);
deferred.resolve(user);
spyOn(apiCall, 'getData').and.returnValue(deferred.promise);
});
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.