Testing AngularJS services that have dependencies to spy on - javascript

The trouble comes when creating the provider for the mock dependency, as it needs to use $q, which is another service within angular, and these cannot be accessed when setting up the provider.
Imagine that we have a Factory we want to test:
angular.module('myApp').factory('MyFactory', function (MyDependency, $q) {
return {
doSomething: function () {
var deferred = $q.defer();
MyDependency.doAction().then(function (response) {
deferred.resolve(response);
// other events
}, function (error) {
deferred.reject(error);
// other events
});
return deferred.promise;
}
}
});
And the following unit test:
describe('Service: MyFactory', function () {
var myDependency, myFactory;
beforeEach(module('myApp'));
// The problem is here, as $q cannot be instantiated
// when setting up providers, and our mock service we are
// creating as the dependency for MyFactory requires $q
beforeEach(module(function ($provide, $q) {
var promise = $q.defer().promise;
myDependency = jasmine.createSpyObj('MyDependency', ['open']);
myDependency.open.andReturn(promise);
$provide.value('MyDependency', {
doAction: myDependency.open
});
}));
beforeEach(inject(function (MyFactory) {
myFactory = MyFactory;
}));
describe('MyDependency.doAction should be called', function () {
myFactory.doSomething();
expect(myDependency.open).toHaveBeenCalled();
// expect other events
});
});
MyDependency has a function, open, that we need to watch and override the method with a custom promise that we will control the data being resolved and rejected. We can easily create the mock dependency that will be injected into MyFactory, but how can we access other services, like $q during this phase?
The only reasonable solution I've come up with is to set up the provider like so, but it gives us much less control and more workarounds to handle success vs failure compared to promise.reject() promise.resolve()
beforeEach(module(function ($provide) {
myDependency = jasmine.createSpyObj('MyDependency', ['doAction']);
myDependency.doAction.andCallFake(function (){
return {
then: function (success, err){
success.call(this);
}
};
});
$provide.value('MyDependency', {
open: myDependency.open
});
}));

Assumptions
I know almost nothing about MyDependency service
It is promise
I can only test behaviour of resolve or reject
The most strange line is $provide.value('MyDependency', {});. This is proof of concept, any comments are welcome.
First revision of the implementation
(function() {
angular.module('myApp', []).factory('MyFactory', function(MyDependency, $q) {
return {
doSomething: function() {
var deferred = $q.defer();
MyDependency.doAction().then(function(response) {
deferred.resolve(response);
}, function(error) {
deferred.reject(error);
});
return deferred.promise;
}
};
});
})();
describe('myApp', function() {
var MyFactory, MyDependency = {},
$q, scope;
beforeEach(function() {
module('myApp');
});
beforeEach(function() {
module(function($provide) {
$provide.value('MyDependency', {});
});
});
beforeEach(inject(function(_MyFactory_, _$q_, $rootScope) {
MyFactory = _MyFactory_;
$q = _$q_;
scope = $rootScope.$new();
}));
describe('MyDependency', function() {
var MyDependencyDefer;
beforeEach(inject(function(MyDependency, $q) {
MyDependencyDefer = $q.defer();
MyDependency.doAction = jasmine.createSpy('MyDependency.doAction').and.returnValue(MyDependencyDefer.promise);
}));
it('resolves doAction()', function() {
var stubSuccess = 'mock data';
var doSomethingDefer = MyFactory.doSomething();
MyDependencyDefer.resolve(stubSuccess);
doSomethingDefer.then(function(r) {
expect(r).toBe(stubSuccess);
});
scope.$digest();
});
it('rejects doAction()', function() {
var stubError = 'reason error';
var doSomethingDefer = MyFactory.doSomething();
MyDependencyDefer.reject(stubError);
doSomethingDefer.catch(function(r) {
expect(r).toBe(stubError);
});
scope.$digest();
});
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine.css" rel="stylesheet" />
<script src="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine-2.0.3-concated.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-mocks.js"></script>
Second revision of the implementation (after two years of development)
(() => {
angular.module('myApp', []).factory('MyFactory', ['MyDependency', MyDependency => {
return {
doSomething: () => MyDependency.doAction()
}
}])
})()
describe('MyFactory.doSomething() returns MyDependency.doAction()', () => {
'use strict';
const promiseResponse = {
succ: 'success',
err: 'error'
}
let MyFactory, MyDependency = {},
$q, scope
beforeEach(module('myApp'));
beforeEach(() => {
module(['$provide', $provide => {
$provide.value('MyDependency', {})
}]);
});
beforeEach(inject(['MyFactory', '$q', '$rootScope', (_MyFactory_, _$q_, $rootScope) => {
MyFactory = _MyFactory_
$q = _$q_
scope = $rootScope.$new()
}]));
describe('MyDependency.doAction() returns promise', () => {
let MyDependencyDefer, doSomethingDefer
beforeEach(inject(['MyDependency', '$q', (MyDependency, $q) => {
MyDependencyDefer = $q.defer()
MyDependency.doAction = jasmine.createSpy('MyDependency.doAction').and.returnValue(MyDependencyDefer.promise)
}]))
beforeEach(() => {
doSomethingDefer = MyFactory.doSomething()
})
it('that can be resolved', done => {
MyDependencyDefer.resolve(promiseResponse.succ)
doSomethingDefer.then(r => {
expect(r).toBe(promiseResponse.succ)
done()
});
scope.$digest()
});
it('that can be rejected', done => {
MyDependencyDefer.reject(promiseResponse.err)
doSomethingDefer.catch(r => {
expect(r).toBe(promiseResponse.err)
done()
})
scope.$digest()
})
})
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine.css" rel="stylesheet" />
<script src="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine-2.0.3-concated.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-mocks.js"></script>

Related

How do I get then on $http to resolve on an Karma AngularJS automated test so that it passes?

Here's a simplified version of my code:
Plunker: https://plnkr.co/edit/ZlX8oV2bMtzXrxtwGX8q?p=preview
angular.module('test', []).factory('test', function ($http) {
var service = {};
service.load = function () {
return $http.get('api/test');
};
return service;
})
describe('test', function () {
var test;
var $httpBackend;
var $q;
var $rootScope;
beforeEach(angular.mock.module('test'));
beforeEach(inject(function (_test_, _$httpBackend_, _$rootScope_) {
test = _test_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should be defined', function (done) {
var expected = 123;
$httpBackend.whenGET('api/test').respond(expected);
var promise = test.load();
promise.then(function (result) {
expect(result.data).toBe(expected);
done();
});
$httpBackend.flush();
$rootScope.$apply();
});
});
I'm already calling $rootScope.$apply() like this answer suggests.
The test runner is timing out. That means done() is never getting called. That means the function passed into then is never getting called. How do I get that function called? I would expect triggering a digest cycle to do it.
The first argument to respond should be the status code.
$httpBackend.verifyNoOutstandingExpectation(); was giving an error, so I took that out.
angular.module('test', []).factory('test', function ($http) {
var service = {};
service.load = function () {
return $http.get('api/test');
};
return service;
})
describe('test', function () {
var test;
var $httpBackend;
var $q;
var $rootScope;
beforeEach(angular.mock.module('test'));
beforeEach(inject(function (_test_, _$httpBackend_, _$rootScope_) {
test = _test_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingRequest();
});
it('should be defined', function (done) {
var expected = 123;
$httpBackend.whenGET('api/test').respond(200, expected);
var promise = test.load();
promise.then(function (result) {
expect(result.data).toBe(expected);
done();
console.log('here');
});
$httpBackend.flush();
});
});

Testing method in angular resolve

I am writing the test case for testing a service call in angular. I am returning a variable value in the resolve. My issue is that i am getting an undefined value as a response after the promise is resolved. following is the piece of code i am using.
resolve: {
peopleData: ($q, service, apiOperations, errorService) => {
const getHelloWorld = () => {
{
return‘ hello - world ';
}
};
const errorHandler = (error, operation) => {
var deferred = $q.defer();
errorService.handle(error, operation);
deferred.resolve({});
return deferred.promise;
};
service.getPeopleName().then((response) => {
console.log('called then method');
if (!angular.isUndefined(response)) {
return response;
} else {
return {};
}
}).catch((error) => {
errorHandler(error, apiOperations.GET_PEOPLE);
return {};
});
}
Now my test case is
describe('state peopleService application', function() {
let $q, authService, $state, state_name = 'main.application',
result, $rootScope, applicationService;
let errorService = {
handle: function(error, operation) {}
};
let apiOperations = {
GET_People: ‘getPeople ',
TEST_ERROR: 'testError'
};
angular.module('mock.Service', []).service('mockPeopleService', function() {
var peopleService = {};
peopleService.getPeople = function() {
let deferred = $q.defer();
deferred.resolve(‘dummy response ');
return deferred.promise;
}
return applicationService;
}); beforeEach(angular.mock.module(home)); beforeEach(angular.mock.module('mock.Service')); beforeEach(angular.mock.inject(function(_$q_, _$state_, _mockPeopleService_, _$rootScope_) {
$q = _$q_;
$state = _$state_;
$rootScope = _$rootScope_;
peopleService = _mockPeopleService_;
})); it('should load the data', () => {
let state = $state.get(state_name);
let result = state.resolve.peopleData($q, peopleService, apiOperations, errorService);
$rootScope.$apply();
console.log('result ' + result);
});
})
});
The log.console is returning result undefined
Remember that in an arrow function with {curly braces}, a return needs to be explicit, just like in regular functions. So with resolve.peopleData() as currently written, undefined is hardly surprising.
Adding a return and tidying, you might end up with something like this :
resolve: {
peopleData: ($q, service, apiOperations, errorService) => {
return service.getPeopleName().then(response => {
^^^^^^
if (angular.isUndefined(response)) {
throw new Error('response is undefined');
}
return response;
}).catch(error => {
errorService.handle(error, apiOperations.GET_PEOPLE);
return {};
});
}
}
Now, the test stands at least a chance of working.

Angular testing controller using mock factory which returns promise

I'm trying to test an Angular controller and mock a factory so that I can use it within this same test. I'm fairly new to Angular testing & have been having trouble figuring out how to this. My factory, doesn't use the $http rather the $q service, returns a promise. I'm also unsure of what to put inside my mock factory given that the factory's function calls return a promise.
My end goal is to call my mock factory from my controller and then check the two arrays in my controller for the data which is supposed to populate them. If you have any tips for restructuring my tests for testability, please do give feedback.
Angular Controller
export class workListController {
constructor(dataService, $q) {
this.$q = $q;
this.work = [];
this.tasks = [];
this.dataService = dataService;
this.setup();
}
setup() {
this.$q.all([this.dataService.getWorkItems(), this.dataService.getTasks()])
.then(() => {
this.work = this.dataService.getState().work;
this.tasks = this.dataService.getState().tasks;
this.dataService.addNumberOTasksToWork();
});
}
tasksForWork(workId) {
var workTasks = [];
for (let task of this.tasks) {
if (task.agf__Work__c === workId) {
workTasks.push(task);
}
}
return workTasks;
};
}
Angular Factory
const dataService = ($q) => {
let work = [];
let tasks = [];
let connection = new Connection{/**/};
return { getWorkItems, getTasks, addNumberOTasksToWork, getState};
function queryWrapper(query) {
var deferred = $q.defer();
connection.query(query)
.then(function(result) {
deferred.resolve(result);
}, function(error) {
deferred.reject(error);
});
return deferred.promise;
}
function getWorkItems() {
return queryWrapper(`SELECT Id, ......`)
.then((data) => {
//data looks like this: {totalSize: 3, done: true, records: [......]}
work = data.records;
});
}
function getTasks() {
return queryWrapper(`SELECT Id,...`)
.then((data) => {
//data looks like this: {totalSize: 3, done: true, records: [......]}
tasks = data.records;
});
}
function addNumberOTasksToWork() {
work.forEach((workItem) => {
workItem.numberOfTasks = 0;
});
work.forEach((workItem) => {
tasks.forEach((taskItem) => {
if (taskItem.agf__Work__c === workItem.Id) {
workItem.numberOfTasks++;
}
});
});
}
function getState(){
return {work,tasks};
}
};
export {dataService};
Test file
import {workList} from './work-list.module.js';
import {workListDirective} from './work-list.directive.js';
import template from './work-list.html';
import {workListController} from './work-list.controller.js';
describe('AA_TaskBoard - workList', function () {
let $scope;
let $controller;
let $httpBackend;
let mockDataService;
beforeEach(angular.mock.module(workList.name));
//trying to mock factory
beforeEach(angular.mock.module(function($provide) {
$provide.value('dataService', mockDataService);
mockDataService = {
getWorkItems: function(){
//this should return a promise, but unsure of what to put here
return {
};
},
getTasks: function(){
return {
};
}
};
}));
beforeEach(inject(function(_$rootScope_, _$controller_, _$httpBackend_) {
$rootScope = _$rootScope_;
$controller = _$controller_;
$httpBackend = _$httpBackend_;
}));
});

TypeScript/Angular $q immediately resolve defer not working

I have an Angular Service class that I created in TypeScript and I this service has a load method. This particular service the list that it is loading is actually hard coded, so I don't need to load it from any backend service. I would like the load method to return a promise though because I want to service to look like the other data services that I have in the class.
Here is the data service that I have
module MyApplication.Data {
export interface IDestinationService {
load(): ng.IPromise<Array<MyApplication.Models.Destination>>;
}
export class DestinationService implements IDestinationService {
items: Array<MyApplication.Models.Destination>;
constructor($http: ng.IHttpService, private $q: ng.IQService) {
this.items = new Array<MyApplication.Models.Destination>();
this.items.push(new MyApplication.Models.Destination());
this.items.push(new MyApplication.Models.Destination());
this.items[0].Id = 2;
this.items[0].Description = 'Item 1';
this.items[1].Id = 3;
this.items[1].Description = 'Item 2';
}
load(): ng.IPromise<Array<MyApplication.Models.Destination>> {
var defer = this.$q.defer();
defer.resolve(this.items);
return defer.promise;
}
}
}
From what I have read this should make the service work. It will return a promise, but the promise will be immediately resolved when it is returned, so the then method should fire.
I have a Jasmine test class that looks like the following:
module MyApplication.Tests {
describe('Data', () => {
describe('Destination', () => {
var $http: ng.IHttpService;
var $httpBackend: ng.IHttpBackendService;
var $q: ng.IQService;
beforeEach(inject((_$http_: ng.IHttpService, _$httpBackend_: ng.IHttpBackendService, _$q_: ng.IQService) => {
$http = _$http_;
$httpBackend = _$httpBackend_;
$q = _$q_;
}));
describe('', () => {
var results: Array<MyApplication.Models.Destination>;
beforeEach((done) => {
var service = new MyApplication.Data.DestinationService($http, $q);
service.load()
.then((result) => {
results = result;
done();
});
});
it('Returns Planning Brokers list', () => {
expect(results.length).toBe(2);
});
});
});
});
}
But when I run this test I'm getting an Async timeout error from Jasmine because the then method is never firing. How can I get this to work correctly.
You shouldn't need the second describe and beforeEach blocks. Resolve the promise using rootScope.$digest and restructure your test code like this:
describe('Data', () => {
describe('Destination', () => {
var $http: ng.IHttpService;
var $httpBackend: ng.IHttpBackendService;
var $q: ng.IQService;
beforeEach(inject((_$http_: ng.IHttpService, _$httpBackend_: ng.IHttpBackendService, _$q_: ng.IQService) => {
$http = _$http_;
$httpBackend = _$httpBackend_;
$q = _$q_;
}));
it('Returns Planning Brokers list', () => {
var results: Array<MyApplication.Models.Destination>;
var service = new MyApplication.Data.DestinationService($http, $q);
service.load().then((results) => {
expect(results.length).toBe(2);
});
$rootScope.$digest();
});
});
});

AngularJS : How to test a promise resolved in a callback function

I have written a function which opens an indexedDB. I'm using a promise, because I think it is the cleanest way to handle this.
this.$get = function($q, $rootScope, $window) {
return {
finalize: function() {
var deferred = $q.defer();
var dbRequest = $window.indexedDB.open(dbName, dbVersion);
dbRequest.onupgradeneeded = function(event) {
// XXX do something here.
};
dbRequest.onsuccess = function(event) {
db = event.target.result;
console.log('Success!');
deferred.resolve(event);
};
dbRequest.onerror = deferred.reject;
return deferred.promise;
}
};
};
In order to test this, I have created the following test function:
describe('finalize', function() {
it('should initialize the database', function(done) {
var promise = resource.finalize();
promise.then(function() {
console.log('resolved');
var transaction = resource.db.transaction(['Component']);
expect(transaction).toBeDefined();
done();
});
$rootScope.$apply();
});
});
This prints 'Success!' in the console, but the promise is never resolved.
If I move $rootScope.$apply() to the end of the onsuccess function, the promise is resolved, but only for one test. Other tests then throw an error Error: [$rootScope:inprog] $digest already in progress.
How should I resolve this promise? Is a callback function better?
this.$get = function($q, $rootScope, $window) {
return {
finalize: function() {
var deferred = $q.defer();
var dbRequest = $window.indexedDB.open(dbName, dbVersion);
dbRequest.onupgradeneeded = function(event) {
// XXX do something here.
};
dbRequest.onsuccess = function(event) {
db = event.target.result;
console.log('Success!');
$rootScope.$apply(function () {
deferred.resolve(event);
});
};
dbRequest.onerror = function (error) {
$rootScope.$apply(function () {
deferred.reject(error);
});
}
return deferred.promise;
}
};
};
Also you should use var promise = resource.finalize(); one time for all tests

Categories

Resources