I am trying to ensure my unit test case covers the scenario of a promise never being resolved, and I am having some issues.
A code block is worth a thousand words, so..
it('returns my resource', function() {
var myUser = {name:'Bob'}
$httpBackend.expectGET('/myresource').respond(200,myUser);
myService.getMyUser()
.then(function(data) {
expect(data).toEqual(myUser);
},fail);
});
This is all well and good, and tests that the response is as expected and also fails the test should the promise be rejected.
However, should the promise never be resolved at all, the test passes and I would like it to fail. I am caching this request in myService like so:
var cachedUser;
function getMyUser(forceUpdate) {
var deferred = $q.defer();
if (cachedUser && !forceUpdate) {
deferred.resolve(cachedUser);
} else {
$http.get('/myresource')
.then(function(data) {
cachedUser = data;
deferred.resolve(data);
},function(error) {
deferred.reject(error);
});
}
return deferred.promise;
}
Now in the above scenario, if one were to remove the line "deferred.resolve(data)" from within the $http.get, the test would still pass. This is because the callback function containing the expectation for data.toEqual(myUser) is never run. However the test should fail because removing that line breaks the purpose of this function.
I have tried within the test code to make the positive callback a spy and expect the spy toHaveBeenCalled, but that seems to run before the promise is resolved and fails even though I can see that the spy's function ran via a console log.
I have also tried within the unit test putting the promise in a variable and then expect(promise.$$state.status).toEqual(1), but again it appears that this expectation is run before the promise is resolved and so the test fails when it should pass.
Please be sure you understand the problem before answering, post a comment if I have not been clear.
The issue turned out to be that when dealing with $httpBackend, you must flush() the backend for requests to go through. I was doing this in an afterEach() block around every test, and so when trying to test the state of the promise from within the test block the promise had not actually been resolved yet.
By moving $httpBackend.flush() to within the unit test, the promise returned did indeed update its status from 0 to 1.
Here is the final unit test code:
it('returns my resource', function() {
var myUser = {name:'Bob'}
var promise;
$httpBackend.expectGET('/myresource').respond(200,myUser);
promise = myService.getMyUser()
.then(function(data) {
expect(data).toEqual(myUser);
},fail);
$httpBackend.flush();
expect(promise.$$state.status).toEqual(1);
});
Related
I have a problem with a small karma unit test that should check a simple decryption/encryption service.
The thing is, if I call the following code "manual" (i.e., within my running angular app) everything is fine and I receive the expected test output:
this.encryptDataAsync('Hello World of Encryption','b4b63cd1a64dbef72fefe2eb3e3fc3eb').then((encryptedValue : string) : void => {
console.log('1',encryptedValue);
this.decryptDataAsync(encryptedValue,'b4b63cd1a64dbef72fefe2eb3e3fc3eb').then(function(decryptedValue : string) : void{
console.log('2',decryptedValue);
});
});
As soon as I try to run this Karma/Jasmine unit test
describe('simple encryption/decryption', function() {
var results = '';
beforeEach(function(done) {
_cryptoService.encryptDataAsync('ABC','b4b63cd1a64dbef72fefe2eb3e3fc3eb').then(function (encryptedValue){
console.log('1');
_cryptoService.decryptDataAsync(encryptedValue,'b4b63cd1a64dbef72fefe2eb3e3fc3eb').then(function(decryptedValue){
console.log('2');
results = decryptedValue;
done();
});
});
});
it("check results", function(done){
expect(results).toBe('ABC');
done();
}, 3000);
});
I never reach console.log('1') nor '2'. I can confirm this while debugging the unit test. However, this is the only unit test that fails in the complete suite, so I guess it won't by a problem with modules, etc.
Is there a general problem with my test case? I would have expected that I can use the then functions to handle my test case and, afterwards, call the done() function to invoke the assertion part.
Update/Edit:
The service uses webcrypto as a library. It is complete independent of angular besides being an angular service (so, no variables on scopes, etc)
I needed to call scope.apply since "$q is integrated with the $rootScope.Scope Scope model observation mechanism in angular, which means faster propagation of resolution or rejection into your models and avoiding unnecessary browser repaints, which would result in flickering UI."
hmmm, i am stuck on this for a while, hopefully i can get some hints from you guys.
i put some sample code here to illustrate the issue:
http://jsfiddle.net/HB7LU/18216/
so basically, i am expecting console.log('out put data for member ' + number); get executed right after mySvc.get() rest call finishes every time i click on the member item.
$q.all([
mySvc.get({id: number})
]).then(function() {
console.log('out put data for member ' + number);
});
but it is not the case, it only works as expected the first time you click on it. second time you click on it, the opposite happens.
XHR finished loading: GET "http://fiddle.jshell.net/HB7LU/18216/show/test?id=1"
(index):53 loading data - rest call finished
(index):68 out put data for member 1 <- this is correct
(index):68 out put data for member 2 <- this is wrong, should wait till rest call finishes
XHR finished loading: GET "http://fiddle.jshell.net/HB7LU/18216/show/test?id=2
(index):53 loading data - rest call finished
ps: i tested it in chrome. havent tested it in other browsers
please let me know how i can fix it. thanks!
You are returning the same deferred each time instead of creating a new one, if you moved the creation of the deferred inside the get call or just returned the promise from the $http call it works fine
myService.get = function(requestParams) {
var deffered = $q.defer();
http://jsfiddle.net/jc04arnn/
You need to create a new deferred each time they call .get(). See the corrected code. Because you can only resolve a deferred once, your resolving it multiple times has no event. It only resolves it the first time. After that, any time it returns that promise, it will immediately fire the .then, cause the deferred is already resolved.
myService.get = function(){
var deffered = $q.defer();
http://jsfiddle.net/8qLrnz5o/
Other solution is return the promise that $http creates (first put the callback and then return the promise) :
myService.get = function(requestParams) {
var call = $http({
method: 'GET',
url: TEST_DATA_URLS,
params: requestParams
});
call.success(function (msg) {
console.log('loading data - rest call finished');
});
return call;
};
when you call mySvs.get you have the same promise:
mySvc
.get({id: number})
.then(function() {
console.log('out put data for member ' + number);
});
Here is the fiddle:
http://jsfiddle.net/tmbs0b1L/
All my UNIT tests, not E2E tests, that do an explicit rootScope.digest() or httpBackend.flush() to flush the asynchronous callback, experience the error:
How to avoid the 'Error: Unexpected request: GET'
No more request expected
I reckon it is because httpBackend calls the ui-router template. I don't know why it wants to do so. I'm not asking for this. I only want it to call my mocked json service.
This error forces me to have the following statement in each it() block:
$httpBackend.whenGET(/\.html$/).respond('');
There must be a neater way.
Specially if the test has no use of the $httpBackend in the first place:
it('should return the list of searched users', function() {
// Always use this statement so as to avoid the error from the $http service making a request to the application main page
$httpBackend.whenGET(/\.html$/).respond('');
var users = null;
UserService.search('TOTO', 1, 10, 'asc', function(data) {
users = data.content;
});
$rootScope.$digest();
expect(users).toEqual(RESTService.allUsers.content);
});
The test passes but it looks hackish. Or noobish :-)
EDIT: Another test:
it('should return the list of users', function () {
// Always use this statement so as to avoid the error from the $http service making a request to the application main page
$httpBackend.whenGET(/\.html$/).respond('');
// Create a mock request and response
$httpBackend.expectGET(ENV.NITRO_PROJECT_REST_URL + '/users/1').respond(mockedUser);
// Exercise the service
var user = null;
RESTService.User.get({userId: 1}).$promise.then(function(data) {
user = data;
});
// Synchronise
$httpBackend.flush();
// Check for the callback data
expect(user.firstname).toEqual(mockedUser.firstname);
expect(user.lastname).toEqual(mockedUser.lastname);
});
This is obviously by design, your tests should be checking that HTTP calls are being made and that they're requesting the correct URL. Instead of checking whether requests are made to /\.html$/ why not instead check whether requests are made to the correct endpoints? Whether that be a directives partial or an API call.
If you insist on throwing away what could be a useful test, you could move your whenGET() to a beforeEach().
I am following the help on AngularJS's documentation for Q's implementation $q. I tried out the following code from https://docs.angularjs.org/api/ng/service/$q
// for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet`
// are available in the current lexical scope (they could have been injected or passed in).
function asyncGreet(name) {
var deferred = $q.defer();
setTimeout(function() {
// since this fn executes async in a future turn of the event loop, we need to wrap
// our code into an $apply call so that the model changes are properly observed.
scope.$apply(function() {
deferred.notify('About to greet ' + name + '.');
if (okToGreet(name)) {
deferred.resolve('Hello, ' + name + '!');
} else {
deferred.reject('Greeting ' + name + ' is not allowed.');
}
});
}, 1000);
return deferred.promise;
}
var promise = asyncGreet('Robin Hood');
promise.then(function(greeting) {
alert('Success: ' + greeting);
}, function(reason) {
alert('Failed: ' + reason);
}, function(update) {
alert('Got notification: ' + update);
});
My understanding is that the $scope.apply here is to give the callback Angular's context and make sure variables under $scope are accessible.
But below on comparing $q and Kris Kowal's Q, the test code goes:
it('should simulate promise', inject(function($q, $rootScope) {
var deferred = $q.defer();
var promise = deferred.promise;
var resolvedValue;
promise.then(function(value) { resolvedValue = value; });
expect(resolvedValue).toBeUndefined();
// Simulate resolving of promise
deferred.resolve(123);
// Note that the 'then' function does not get called synchronously.
// This is because we want the promise API to always be async, whether or not
// it got called synchronously or asynchronously.
expect(resolvedValue).toBeUndefined(); // <= so the deferred is not resolved without the 'apply'?
// Propagate promise resolution to 'then' functions using $apply().
$rootScope.$apply();
expect(resolvedValue).toEqual(123);
}));
It says $rootScope.$apply() is to propagate the promise resolution to 'then'. I am confused there... So without using apply, the deferred.resolve will not actually resolve?
This documentation sucks.
So the thing with $q is that when your promise is resolved (and also presumably on reject or notify), it invokes your handlers within $rootScope.evalAsync which ensures that after invocation, it'll trigger a digest and thus the rest of your application can have a chance to update or otherwise respond to the changes, just the way we like it.
As you found out, it works just fine without the explicit $apply in an example app. However, the reason why they are doing explicit $apply is because that automagic with $evalAsync doesn't get a chance to work when running synchronously in a test, not because it's required for your application to Just Work ™.
With a few other notable services that are augmented for testing in angular-mock.js, like $http and $timeout, we can explicitly flush when we want to simulate an async http request/response or a timeout (for example). The equivalent to stuff waiting to be evaled is to trigger a digest, which will get your promise handler invoked in the proper context. This is done with $apply or $digest, and hence why you're seeing it in their examples... because their examples are written as synchronous tests.
The docs should explain the difference between what you need to do to get your tests working and what your application itself should focus on to get the job done. The docs have a bad habit of making test facts their examples, and it just confuses people.
Hope that helps.
I'm learning about Angular JS and on the moment I'm trying to understand about promises and async programming and I have this doubt about $q.defer(). My point is the following: usually when people work with promises they do something like that, considering that $q is already available
function someAsyncFunction() {
var deferred = $q.defer();
/* Do things and if everything goes fine return deferred.resolve(result)
otherwise returns deferred.reject()
*/
return deferred.promise;
}
What is this really doing? When we do var deferred = $q.defer() it imediately switches all the execution of that function to another thread and return the promise being a reference to the results of this operation that is still performing there?
Is this the way we should think about when creating async methods?
With $q u run functions asynchronously.
Deferred objects signals that something, some task is done.
var defer = $q.defer(); // we create deferred object, which will finish later.
defer.promise // we get access to result of the deferred task
.then( // .then() calls success or error callback
function(param) {
alert("i something promised " + param);
return "something";
}); // u can use 1 or more .then calls in row
defer.resolve("call"); //returns promise
Here example:
http://jsfiddle.net/nalyvajko/HB7LU/29048/
Angular's $q service is based on the Javascript library Q. You can read more about it in the Q documentation, or read the code in the github repo. I think this part snipped from the introduction to the documentation explains it best:
If a function cannot return a value or throw an exception without
blocking, it can return a promise instead. A promise is an object that
represents the return value or the thrown exception that the function
may eventually provide. A promise can also be used as a proxy for a
remote object to overcome latency.