I want to write a unit test that just displays the behavior of creating two promises, wrapping them up into one with $q.all and then test that the promises are both resolved at the same time.
describe("Application controller", function() {
var scope;
var controller;
beforeEach(module('my.namespace'));
//this suite can be removed.
describe("Application ", function() {
beforeEach(
inject(function ($rootScope, $controller,$q) {
scope = $rootScope.$new();
controller = $controller('Application', {
'$scope': scope
});
}));
it("should package two promises into one", function (done) {
inject(function ($rootScope) {
setTimeout(function () {
$rootScope.$apply(function () {
var promiseOne = $q.defer(),
//promiseOneData;
promiseTwo = $q.defer();
//promiseTwoData
promiseOne.then(function(data){
promiseOne.resolve('promiseOne');
expect(1).toBe(1);
});
promiseTwo.then(function(data){
promiseTwoData.resolve('promiseTwo');
})
var allPromises = $q.all([promiseOne,promiseTwo]);
allPromises.then(function(data){
//data should contain an array of two empty elements for each promise
expect(data.length).toBe(2);
});
done();
});
}, 1000);
})
});
With this I get the error: Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL. I don't want to actually use a get request for anything here, I just need two promises to be resolved and then moved into one promise that contains both. How can I do that with Angular and jasmine?
What you want is probably a spy and to see if it has been called or not.
describe('test $q', function() {
var
$scope;
$controller;
$httpBackend;
beforeEach(function() {
module('myModule');
inject(function(_$rootScope_, _$controller_) {
$scope = _$rootScope_.$new();
$controller = _$controller_;
$controller ('MyCtrl', {
$scope: $scope
});
});
it('should test that $q.all calls callback when dependent promises are resolved', function() {
var deferOne = $q.defer(),
deferTwo = $q.defer();
combinedSpy = jasmine.createSpy('combined');
$q.all([deferOne.promise, deferTwo.promise]).then(combinedSpy);
expect(combinedSpy).toNotHaveBeenCalled();
deferOne.resolve();
deferTwo.resolve();
$scope.apply();
expect(combinedSpy).toHaveBeenCalled();
});
This test title is pretty confusing, you are not simulating a promise. You are testing a promise. And you don't have to, there are already tests for $q in Angular itself, so there is no point in you writing tests for that?
wrapping them up into one with $q.all
That creates a third promise. The third promise is resolved when both promise A and B have been resolved.
test that the promises are both resolved at the same time
JavaScript is single threaded, they cannot be resolved at the same time. The third promise, the one that is created by $q.all(), will be resolved when the first and second have both been resolved. Time may pass between A and B being resolved.
Say that A is resolved, an hour later B is resolved. Then C ($q.all) will be resolved in the next digest cycle (by $scope.apply()).
This is the OP's answer that they pasted into the question:
it("Should simulate promise",inject(function($q, $rootScope){
var deferred = $q.defer();
var promise = deferred.promise;
var resolvedValue;
promise.then(function(value){
resolvedValue = value;
});
//made no promises yet
expect(resolvedValue).toBeUndefined();
var application = {
id: '123',
name: 'suebalu',
database: '234',
folder: 'c:',
version: '1.2',
file: 'previewPubFile',
date: '2009-01-01'
};
deferred.resolve({
id: '123',
name: 'suebalu',
database: '234',
folder: 'c:',
version: '1.2',
file: 'previewPubFile',
date: '2009-01-01'
});
$rootScope.$apply();
expect(resolvedValue).toEqual(application);
}));
Related
Environment:
Angular 1.5.8
Unit Tests with Karma/Jasmine
This is my controller, which aims to get a value from $stateParams and uses that, to perform a DELETE request, later on.
Right now it should ask the user, wether to delete the object.
This is done with a sweetalert.
I have removed ngdoc comments and arbitrary SWAL config.
ClientDeleteController.js:
angular.module('app.data').controller('ClientDeleteController', [
'$stateParams', '$q',
function ($stateParams, $q) {
'use strict';
var vm = this;
vm.clientId = $stateParams.id;
vm.promptDeferred = null;
vm.prompt = function () {
// create promise
var d = $q.defer();
vm.promptDeferred = d;
// create prompt
swal({ .... }, vm.swalCallback);
};
vm.swalCallback = function (confirmed) {
if (confirmed) {
console.info('resolving...');
vm.promptDeferred.resolve();
} else {
console.info('rejecting...');
vm.promptDeferred.reject();
}
};
vm.delete = function () {
vm.prompt();
vm.promptDeferred.promise.then(vm.performDelete);
};
vm.performDelete = function () {
console.info('performing');
};
}]);
This is the test suite:
ClientDeletecontrollerSpec.js
describe('Controller:ClientDeleteController', function () {
var controller
, $httpBackend
, $rootScope
, $controller
, scope
, $q
, $stateParams = {id: 1}
, resolvePromise = true
;
swal = function (options, callback) {
console.info('i am swal, this is callback', callback+'', resolvePromise);
callback(resolvePromise);
};
beforeEach(function () {
module('app');
module('app.data');
module('ui.bootstrap');
module(function ($provide) {
$provide.service('$stateParams', function () {
return $stateParams;
});
});
inject(function (_$controller_, _$rootScope_, _$q_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$q = _$q_;
scope = $rootScope.$new();
controller = $controller('ClientDeleteController', {$scope: scope, $q: $q});
});
});
describe('basic controller features', function () {
it('should be defined', function () {
expect(controller).toBeDefined();
});
it('should get the client id from the state params', function () {
expect(controller.clientId).toBeDefined();
expect(controller.clientId).toEqual($stateParams.id);
});
});
fdescribe('client delete process', function () {
it('should ask the user if he really wants to delete the client', function () {
spyOn(controller, 'prompt').and.callThrough();
controller.delete();
expect(controller.prompt).toHaveBeenCalled();
});
it('should create a promise', function () {
controller.prompt();
expect(controller.promptDeferred).toBeDefined();
});
it('should delete when the user clicked yes', function () {
spyOn(controller, 'performDelete').and.callThrough();
controller.delete();
expect(controller.performDelete).toHaveBeenCalled();
});
it('should not delete when the user clicked no', function () {
spyOn(controller, 'performDelete').and.callThrough();
resolvePromise = false;
controller.delete();
expect(controller.performDelete).not.toHaveBeenCalled();
});
});
});
The test should delete when the user clicked yes fails, the test should not delete when the user clicked noreturns a false positive.
The console.info(...) within the swal mock logs the correct callback function. The logs in the function itself are also logged, which tells me, the callback is fired.
Since in the next line, I call either vm.promptDeferred.resolve() resp. .reject(), I expect the call to actually happen.
Nonetheless, the test result is Expected spy performDelete to have been called..
I have mocked swal in another test the same way and it works fine.
I don't get why the promise won't be resolved.
Notice: When I don't store the promise directly on the controller but return it from prompt() and use the regular .prompt().then(...), it won't work either.
The logs are the same and I like to split functions as much as possible, so it is easier to understand, easier to test and to document.
There are hundreds of other tests in this application but I can't see, why this one won't work as expected.
Thank you for any insight.
What happens if you do $rootScope.$digest() just after invoking delete in the test?
In my experience something like that is necessary to make angular resolve promises during tests (either that or $rootScope.$apply() or $httpBackend.flush())
I am new to Jasmine. I am trying to get my factory promise to resolve but it never seems to.
Here is my factory which will retuirn a list of companies
angular.module('commercial.factories').factory('companyFactory', companyFactory);
function companyFactory($http, $location, sessionStorage, config) {
function getCompanies() {
//call service and include company identifier in the service url
return $http.get(wunUtils.constants.serviceBaseUrl + 'myCompanyService.svc/companies')
.then(function(response) {
return response.data;
}, function(err){
console.log(err);
});
}
});
and my spec
describe('company', function() {
beforeEach(module('mockedCommercial'));
var $httpBackend, companyFactory, $rootScope
beforeEach(function() {
inject(function(_$httpBackend_, _$rootScope_, _companyFactory_) {
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
companyFactory = _companyFactory_;
});
})
it('should return available languages asynchronously', function() {
$httpBackend.expectGET(/https:\/\/myDomain.com\/myCompanyService.svc\/companies+/)
.respond(function(method, url, data, headers, params) {
var response = {
"data": [{
"address1": "Portsmouth",
"address2": null,
"city": "Portsmouth",
"contactEmail": "mocks#mocks.com",
"contactFirstName": "Mock",
"contactLastName": "Data",
"contactPhone": null,
"contactTitle": "Mr",
"country": "UK",
"fax": null,
"identifier": "c1xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"location": "Portsmouth, UK",
"name": "Mock Company 1",
"phone": null,
"state": "Hampshire",
"zipCode": "PO19 3 EN"
}, {
"address1": "Portsmouth",
"address2": null,
"city": "Portsmouth",
"contactEmail": "mocks#mocks.com",
"contactFirstName": "Test",
"contactLastName": "Test",
"contactPhone": null,
"contactTitle": "Mrs",
"country": "UK",
"fax": null,
"identifier": "c2xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"location": "Portsmouth, UK",
"name": "Mock Company 2",
"phone": null,
"state": "Hampshire",
"zipCode": "Po9 3EN"
}],
"total": 2
}
});
//--------------------------
$rootScope.$digest();
companyFactory.GetCompanies().then(function(result) {
expect(result.hasOwnProperty('total')).toBe(true);
})
});
});
As you can see i am using $rootScope.$digest to run the promise, however my expect is never hit so the test always passes.
I have read loads of different posts about this who all say this should work. Am i doing something wrong here or have i missed some step?
Edited - Simplified approach
So i simplified my question somewhat after looking at some more posts
Here is my new controller call...
$scope.companies = [];
var GetCompanies = function(options) {
companyFactory.GetCompanies().then(function(response) {
options.success(response);
$scope.companies = response
});
}
$scope.GetCompanies = GetCompanies
my factory...
function getCompanies() {
//call service and include company identifier in the service url
return $http.get(wunUtils.constants.serviceBaseUrl + 'CompanyService.svc/companies')
.then(function(response) {
return response.data;
}, function(err){
console.log(err);
});
}
var service = {
AddCompany: addCompany,
GetCompany: getCompany,
GetCompanies: getCompanies,
GetUserCompanyFleets: getUserCompanyFleets,
EditCompany: editCompany
};
and my new spec
describe('company', function() {
beforeEach(module('mockedCommercial'));
var $httpBackend, companyFactory, $rootScope, createController, scope
beforeEach(function() {
inject(function($controller, _$httpBackend_, _$rootScope_, _companyFactory_) {
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
scope = _$rootScope_.$new();
companyFactory = _companyFactory_;
createController = function() {
return $controller('ManageCompaniesController', {
'$scope': scope
});
};
});
})
it('should return available languages asynchronously', function(done) {
$httpBackend.whenGET(/https:\/\/myDomain.com\/V3_Fleet\/CompanyService.svc\/companies+/)
.respond(function(method, url, data, headers, params) {
return [{ 'id': 1 }];
}
});
//--------------------------
var dummy = [{ 'id': 1 }];
createController();
scope.GetCompanies()
$httpBackend.flush()
expect(scope.companies).toEqual(dummy)
scope.$digest(); done()
});
});
Try using $httpBackend.flush() before evaluating the test expectations.
This is the approach recommended by angular. For more details and examples, you can check the documentation here.
You have two options when testing asynchronous code.
1. Use synchronous mocks for everything
This includes:
mock $httpBackend
use $timeout.flush()
use $interval.flush()
use scope.$apply() to process leftover promises
And after all these you still cannot be entirely sure that all pending tasks have been processed, although its sufficient in 99.9% of the time.
2. Make your tests asynchronous
Here you will have to make sure that every asynchronous controller method returns a promise, even if the returned promise is not used in the application. They will be used in tests.
Jasmine makes async testing pretty difficult but not impossible. You will have to use the done callback, and code every asynchronous block like:
it('should...', function(done) {
$q.when().then(function() {
return $controller.GetSomething();
}).then(function() {
expect(scope.companies).toEqual(dummy);
}).then(done, done.fail);
});
Mocha handles proper asynchronous functions so you can write without done:
it('should...', function() {
return $q.when().then(function() {
return $controller.GetSomething();
}).then(function() {
expect(scope.companies).toEqual(dummy);
});
});
Or even use async-await if you have the toolset:
it('should...', async () => {
await $controller.GetSomething();
expect(scope.companies).toEqual(dummy);
});
So after much reading i found the answer by simplifying everything down. I think my issues came from mixing $httpBackend.flush() and the done() function
What seems to happen is the the done() function triggers its own digest, so when trying to use the flush() function there is a conflict.
Here is my working factory. Its a little different from my original purly as this was a more simple function to work with (less things to go wrong)
function getUser(userId) {
//call service and include user identifier in the service url
return $http.get(wunUtils.constants.serviceBaseUrl + 'myService.svc/fleetusers/' + userId)
.then(function(response) {
return response.data;
});
}
var service = {
GetMe: getMe,
GetUser: getUser,
AddUser: addUser,
EditUser: editUser,
ActivateUser: activateUser
};
and here is my simplified spec call the $httpBackend.Flush()
describe('Testing httpBackend', function() {
var $httpBackend;
beforeEach(module('mockedCommercial'));
beforeEach(inject(function(_$httpBackend_) {
$httpBackend = _$httpBackend_;
}))
it('should behave...', function() {
inject(function(userFactory, $rootScope) {
var mockData = [];
$httpBackend.expectGET(wunUtils.constants.serviceBaseUrl + 'FleetUserService.svc/fleetusers/10').respond(mockData);
var promise = userFactory.GetUser(10);
$httpBackend.flush();
promise.then(function(result) {
expect(result).toEqual(mockData);
});
});
});
});
EDIT
It should also be noted that when testing HTTPBackend the test will only work while using the expectGET and expectPOST as opposed to whenGET and whenPOST.
From what i can see, whenGet is used for mocking data while expectGet is used for testing mocks
I'm trying to create a db factory that returns data from the database to the client after the data is successfuly posted but it returns as 'undefined' for some reason.
My factory function looks like this:
uno.factory('adbFactory', ['$http', function($http){
var fact = {};
fact.get = function(http, query, isAll) {
//var query = "get all blog_posts";
http.post('php/adb/adb.php', {'query': query, 'all': isAll})
.success(function(data){
//console.log(data);
return data;
})
.error(function(){
console.log('Error...');
});
};
return fact;
}]);
And my controller resembles this:
uno.controller('newsCtrl', function($scope, $http, adbFactory){
$scope.derp = 'derp!!!!!';
console.log(adbFactory.get($http, 'get users 1', false));
});
don't worry about the 'get users 1 etc etc' string, i created a function in php that renders a SQL query based on given parameters. Is there something in my factory code i need to improve on??
I would advice returning the promise from the factory and handling the success and error events in the controller instead.
fact.get = function(http, query, isAll) {
return http.post('php/adb/adb.php', {'query': query, 'all': isAll});
};
uno.controller('newsCtrl', function($scope, $http, adbFactory){
adbFactory.get($http, 'get users 1', false).success(function(data) {
console.log(data);
});
});
fact.get method has no return statement, that's why it returns undefined.
Also, this callback is useless because it is called asynchronously
.success(function(data){
//console.log(data);
return data;
})
I think you want somethig like:
fact.get = function(http, query, isAll) {
return http.post('php/adb/adb.php', {'query': query, 'all': isAll});
};
uno.controller('newsCtrl', function($scope, $http, adbFactory){
adbFactory
.get($http, 'get users 1', false)
.success(function(data){
console.log(data);
})
.error(function(){
console.log('Error...');
});
});
You have to keep in mind that you are performing some asynchronous request.
You have two way to retrieve your data :
Following the callback way
Following the promise way
As you know, $http service return promise, and has some callback method, like .success() and .then() for example.
For promise, $q.defer() is a promise manager from the deferred API.
$q.defer() get 2 methods :
resolve(value) : which resolve our associated promise, by giving her the final value
reject(reason) : which resolve an promise error.
So you can do :
Service
(function(){
function Service($http, $q){
var defer = $q.defer();
//Callback way
function get(callback){
$http.get('app.php').success(function(data){
//Pass our data to the callback
callback(data);
});
}
//Promise ways
function getPromise(){
$http.get('app.php').success(function(data){
//Resolve the data
defer.resolve(data);
});
//Return our promise
return defer.promise;
}
return {
get: get,
getPromise: getPromise
};
}
angular
.module('app')
.factory('Service', Service);
})();
Controller
(function(){
function Controller($scope, Service) {
//Our callback method
function print(data){
console.log(data);
}
//Retrieve our data by using callback way
Service.get(print);
//Retrieve our data by using promise way
var promise = Service.getPromise();
//When promise is resolved
promise.then(function(data){
//Retrieve our data
console.log(data);
});
}
angular
.module('app', [])
.controller('ctrl', Controller);
})();
But what should i use ? I think that use promise is better than callback, because you can handle easily your request. Moreover, you can perform promise chaining, and avoid the famous callback hell.
I'm newbie to AngularJs/NodeJs world, so forgive if this is a basic question to some.
So in a nutshell I've two controllers, the first controller $broadcast an 'Id' and the second controller fetches that Id with $on and then passes that Id to an intermediate service, which makes an $http ajax call and returns a single Book object.
How do I unit test $scope.broadcast, $scope.$on using Jasmine
firstCtrl
.controller('firstCtrl', function($scope, ...){
$scope.selectGridRow = function() {
if($scope.selectedRows[0].total !=0)
$scope.$broadcast('id', $scope.selectedRows[0].id);//Just single plain ID
};
});
secondCtrl
.controller('secondCtrl',
function($scope, bookService) {
$scope.$on('id', function(event, id) {
bookService.getBookDetail(id).then(function(d) {
$scope.book = d.book;
});
});
});
expected Json obj
var arr = "book" : [ {
"id" : "1",
"name" : "Tomcat",
"edition" : "9.1"
}
]
Let me know if anyone wants me to post the $http service that's used by the second controller.
expected behavior
So from the top of my head, ideally, I would like to test every possible scenario, but something like below, which can then expend:
expect(scope.book).toEqual(arr);
expect(scope.book).not.toEqual(undefined);
Thanks everyone!
First you should do the broadcast on $rootScope then you can receive on $scope.
Now to the testing. I assume you want to include real request to your API via bookService and $http. This can be mocked but I'll focus on the real call. Let me know if you need the mocked one.
Before the actual test, you will need to do some injections/instantiations:
Initialize your app
Inject $controller, $rootScope, $httpBackend and bookService
Create scopes for firstController and SecondController and store it in a variable
Store bookService and $httpBackend in variables
Instantiate the controllers and store them
Then in the actual test you must tell $httpBackend what to do when it caches request for the books (or books). Construct $httpBackend.whenGET("/api/books/1").passThrough(); will pass request with url "/api/books/1" to the server.
Next your must setup property selectedRows on firstScope so it fulfills the condition in function selectGridRow in your firstCtrl.
Now you can call function selectGridRow to trigger the broadcast and API call. But you must wrap it in runs function so Jasmine recognizes this as an async call and will wait for it to finish. The 'waiting' is defined in waitsFor call. It will wait until it gets a book and it waits max 5000 ms then the test will be marked as failed.
Last step is to check expected result. We don't have to check for undefined anymore as the test would not get to here anyway. The check must be wrapped again runs call so it is executed afters successful 'waitsFor'.
Here is the full code:
describe("Broadcast between controllers", function () {
beforeEach(module('app')); //app initialization
var firstScope;
var secondScope;
var bookService;
var $httpBackend;
var firstController;
var secondController;
beforeEach(inject(function ($controller, $rootScope, _bookService_, _$httpBackend_) {
firstScope = $rootScope.$new();
secondScope = $rootScope.$new();
bookService = _bookService_;
$httpBackend = _$httpBackend_;
firstController = $controller('firstCtrl', { $scope: firstScope });
secondController = $controller('secondCtrl', { $scope: firstScope, bookService: bookService });
}));
it("should work", function () {
$httpBackend.whenGET("/api/books/1").passThrough();
firstScope.selectedRows = [{ id: 1, total: 1000 }];
secondScope.book = null;
runs(function () {
firstScope.selectGridRow();
});
waitsFor(function () {
return secondScope.book != null;
}, "Data not received in expected time", 5000);
runs(function () {
expect(secondScope.book[0].id).toEqual(1);
});
});
});
I have tried to create a mock of my BoardService. However, the .then function is not being run in the controller when running the test, but it's working fine when running the actual application.
Here is the test:
beforeEach(inject(function($rootScope, $controller, $q) {
scope = $rootScope.$new();
BoardController = $controller('BoardController', {
$scope: scope,
board: {
id: 1,
tasks: []
},
BoardService: {
addTask: function(data) {
var defer = $q.defer();
defer.resolve(1);
return defer.promise;
}
}
});
}));
it("should add a new task", function() {
scope.addTask(category, 'this is a new task');
expect(scope.board.tasks.length).toBe(1);
});
And the controller function:
$scope.addTask = function(category, task) {
BoardService.addTask({name: task, category: category.id, boardId: $scope.board.id}).then(function(task_id) {
// Never reaches here
$scope.board.tasks.push({id : task_id, name : task, category : category.id, board_id : $scope.board.id});
});
}
Why is it never reaching the comment in the .then function?
After resolving the promise you need to trigger a digest-cycle for angular to pick up on it.
scope.addTask(category, 'this is a new task');
scope.$digest();
expect(scope.board.tasks.length).toBe(1);
As we discussed on IRC: you need to call scope.$digest for the promise to fire.