2

I have a service method that has some caching logic:

model.fetchMonkeyHamById = function(id)
{
    var that = this;
    var deferred = $q.defer();
    if( that.data.monkeyHam )
    {      
         deferred.resolve( that.data.monkeyHam );
         return deferred.promise;
     } else {
          return this.service.getById( id).then( function(result){
             that.data.monkeyHam = result.data;           
         });
    }
};

I know how to use $httpBackend to force the mocked data to be returned. Now, how do I force it to resolve (and then test the result) when I've set the data explicitly?

I want to test the result in the controller then() function:

            MonkeyHamModel.fetchMonkeyHamById(this.monkeyHamId).then( function() {
                $scope.currentMonkeyHam = MonkeyHamModel.data.monkeyHam;
            });

Then my test I want to explicitly set the data (so it loads from memory "cache" instead of httpBackend)

     MonkeyHamModel.data.monkeyHam = {id:'12345'};
     MonkeyHamModel.fetchMonkeyHamById( '12345');
     // How to "flush" the defer right here like I would have flushed httpBackend?
     expect( $scope.currentMonkeyHam.id ).toEqual('12345'); //fails because it is not defined yet since the $q never resolved

Where $scope is just the scope of my controller, but called $scope here for brevity.

UPDATE:

The suggested answer does not work. I need the function to return a promise, not a value that is the result of a promise:

model._monkeyHams = {} // our cache
model.fetchMonkeyHamById = function(id){
 return model.monkeyHams[id] || // get from cache or
        (model.monkeyHams[id] = this.service.getById(id).then(function(result){
            return result.data;        
        }));
};

The following requires that you have touched the server already. I create a model on the front end (currentMonkeyHam) or whatever, and don't load it back after the first POST (an unnecessary GET request). I just use the current model. So this does not work, it requires going out to the server at least once. Therefore, you can see why I created my own deferred. I want to use current model data OR get it from the server if we don't have it. I need both avenues to return a promise.

var cache = null;
function cachedRequest(){
    return cache || (cache = actualRequest())
}
John Slegers
  • 45,213
  • 22
  • 199
  • 169
FlavorScape
  • 13,301
  • 12
  • 75
  • 117

3 Answers3

3

Your code has the deferred anti pattern which makes it complicated - especially since you're implicitly suppressing errors with it. Moreover it is problematic for caching logic since you can end up making multiple requests if several requests are made before a response is received.

You're overthinkig it - just cache the promise:

model._monkeyHams = {} // our cache
model.fetchMonkeyHamById = function(id){
 return model.monkeyHams[id] || // get from cache or
        (model.monkeyHams[id] = this.service.getById(id).then(function(result){
            return result.data;        
        }));
};

In your case, you were caching all IDs as the same thing, the general pattern for caching promises is something like:

var cache = null;
function cachedRequest(){
    return cache || (cache = actualRequest())
}

Creating deferred is tedious and frankly - not very fun ;)

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • On the subject of testing it - if you set `model._monkeyHams[id]` for any specific ID to a value it'll return that value rather than make that request and you can test that logic – Benjamin Gruenbaum Sep 04 '14 at 20:04
  • "you were caching all IDs as the same thing" we only ever have one in this particular case. Others use lists/getters/setters. Regardless, should I just be using HTTP caching instead? http://stackoverflow.com/questions/14117653/how-to-cache-an-http-get-service-in-angularjs – FlavorScape Sep 04 '14 at 20:12
  • @FlavorScape no, promise caching is fine - as you see it's just one to two extra lines of code. It depends. – Benjamin Gruenbaum Sep 04 '14 at 20:16
  • Cool I guess at the outset I was trying to figure out how to return either static data or result of a promise-- the examples I found were all of the anti-pattern flavor. It makes much more sense this way. – FlavorScape Sep 04 '14 at 20:21
  • @FlavorScape people struggle with promises when they start using them, I know I did - you can see a [self contained example](http://stackoverflow.com/questions/23803743/what-is-the-deferred-antipattern-and-how-do-i-avoid-it) of the pattern here and how to resolve it generally. The other more subtle mistake is the race condition created by caching the data and not the promise (this can be avoided but requires two flags) – Benjamin Gruenbaum Sep 04 '14 at 20:23
  • See my update. Neither of these solutions solve the problem at hand. – FlavorScape Sep 05 '14 at 17:11
  • @FlavorScape uhh... if the problem is that it requires that you 'touch the server' just assign to it - like I said: There is nothing stopping you from doing `cache = $q.when(value)`. I think the part you're missing here is how to create an immediately resolved promise for a given value - and that way is calling `$q.when` - for example `var cache = $q.when(3)` will initialize the cached result in the second example to 3. – Benjamin Gruenbaum Sep 05 '14 at 17:56
  • Hrm. So I'd still have to do a if( someData ){ cache = $q.when( someData);} return cache || (cache = actualRequest()) – FlavorScape Sep 05 '14 at 22:44
1

You can use setTimeout (or $timeout) for resolving the promise.

You can modify your code as -

model.fetchMonkeyHamById = function(id)
{
    var that = this;
    var deferred = $q.defer();
    if( that.data.monkeyHam )
    {      
         setTimeout(function() {
            deferred.resolve(that.data.monkeyHam);
         }, 100);
         return deferred.promise;
     } else {
          return this.service.getById( id).then( function(result){
             that.data.monkeyHam = result.data;           
         });
    }
};

EDIT:

Modified as per Benjamin's suggestion -

Using $rootScope.digest() - code should be something like this

MonkeyHamModel.data.monkeyHam = {id:'12345'};
MonkeyHamModel.fetchMonkeyHamById( '12345');
$rootScope.digest();
Prasad K - Google
  • 2,584
  • 1
  • 16
  • 18
  • but the original code works, i.e. it resolves immediately. I thought any forced 'resolution' still works asynchronously since it is still returning the promise (it just happens instantly). – FlavorScape Sep 04 '14 at 18:09
  • so you can defer it to not happen instantly, but I think it shouldn't work as you expected – Prasad K - Google Sep 04 '14 at 18:10
  • weird. it works production, then() gets called and our controller gets the appropriate data from "cache". However, it breaks only in testing, the then() never gets called in Jasmine environment. I tried the $timeout.flush() approach as you suggest and it fixes it for testing environment. I wonder why the plain version does not work in testing. – FlavorScape Sep 04 '14 at 18:36
  • Probably it's not taking from cache, but from server. You may be misinterpreting it as it's taken fron cache. You can debug your code (with breakpoints or console.log) to see which part of your model function code is executed... – Prasad K - Google Sep 04 '14 at 19:26
  • Any observable effects of resolving happen asynchronously - that is guaranteed by the A+ spec, this answer is incorrect. – Benjamin Gruenbaum Sep 04 '14 at 19:54
  • Then what could be the reason that it is not working? – Prasad K - Google Sep 04 '14 at 19:59
  • I know 100% it's not going to the server, because i would have gotten an unexpected get (when I remove the backend mock). And in production, i see no network activity. – FlavorScape Sep 04 '14 at 20:05
  • @Prasad OP is not calling $rootScope.$digest() and they're using an old version of Angular probably. – Benjamin Gruenbaum Sep 04 '14 at 20:05
  • 1
    http://stackoverflow.com/questions/23363014/angularjs-q-deferred-api-order-of-things-lifecycle-and-who-invokes-digest – Benjamin Gruenbaum Sep 04 '14 at 20:06
  • @Benjamin, I have experienced this problem myself in unit tests while mocking some services so, had to manually resolve asynchronously... I've never tried it in a controller though.. Will check your link. – Prasad K - Google Sep 05 '14 at 02:43
  • @Benjamin, I have understood the actual problem... Thanks for correcting my understanding!! :) – Prasad K - Google Sep 05 '14 at 03:15
  • Latest angular, and believe me, i've tried $rootScope.$digest() – FlavorScape Sep 05 '14 at 16:09
  • Ah jesus, the suggestion that I change approach put me on a wild goose chase. I think I picked the right (anti-pattern??) approach for the problem at hand, but I had accidentally tried scope.$digest(), not $rootScope.$digest(); before all of this discussion. – FlavorScape Sep 05 '14 at 17:35
  • I thought the same and updated the answer so you would try... :) Cool!! – Prasad K - Google Sep 05 '14 at 17:38
  • @FlavorScape latest Angular should not be affected by `$rootScope.digest()` - they fixed that need already in recent versions. – Benjamin Gruenbaum Sep 05 '14 at 18:04
0

We've done something similar in our code base, but instead of having an object with state that constantly changed we went with something that looks more like a traditional repository.

someInjectedRepoistory.getMonkeyHamModel().then(x => $scope.monkeyHam = x);

Repository{
   getMonkeyHamModel() {
       var deferred = $q.defer();
       if( this.cache.monkeyHam )
       {      
            deferred.resolve( this.cache.monkeyHam );
        } else {
             return this.service.getById( id).then( function(result){
                this.cache.monkeyHam  = result.data;           
            });
       }
       return deferred.promise
   }
}

There are no problems with returning a completed deferred. That's part of the purpose of the deferreds, it shouldn't matter when or how they are resolved, they handle all of that for you.

As for your test we do something like this.

testGetFromService(){
   someInjectedRepoistory.getMonkeyHamModel().then(x => verifyMonkeyHam(x));
   verifyHttpBackEndGetsCalled()
}

testGetFromCache(){
   someInjectedRepoistory.getMonkeyHamModel().then(x => verifyMonkeyHam(x));
   verifyHttpBackEndDoesNotGetCalled()
}
Zipper
  • 7,034
  • 8
  • 49
  • 66
  • That's what i thought about promise behavior, you can resolve at any time. Our setup is the same, we just call it data, not cache. I cant get the then() to actually fire. I tried the $timeout as suggested in the other answer, then did a $timeout.flush() and it worked like a charm. Please explain how you get the then() to fire. – FlavorScape Sep 04 '14 at 18:19
  • I can't call it directly, as I'm ensuring the controller is using it correctly. SomeCtroller.getData(){someRepository.someGetter.then(function(someData){ //console.log('i was called')}); never traces (in testing, works in production). – FlavorScape Sep 04 '14 at 18:49