19

Is there a way to return an HttpPromise (or something similar) to mimic a call to $http? I want to set a global variable that indicates whether the real HTTP request is made or whether a fake HttpPromise object is returned with fake data.

For example, I have a service that is similar to this:

angular
  .module('myservice')
  .factory('MyService', ['$http', function($http) {
      return {
       get : function(itemId) {
         if (isInTestingMode) {
           // return a promise obj that returns success and fake data
         }
         return $http.get("/myapp/items/" + itemId);
       }
    };
 } ]);

And in my controller, I have a call to the aforementioned service that looks similar to this:

        // Somewhere in my controller

        MyService.get($scope.itemId)
           .success(function(data) {
              $scope.item = data;
           })
           .error(function(data, status, headers, config) {
              $scope.notFound = true;
           });

I'm trying to not change the controller code; I want the success and error chaining to still work when in my "isInTestMode". Is it possible to fake an HttpPromise in the way that I described in the service?


Below is a revised edition of the "MyService" above (a snippet) containing a success and error on the promise object. But, how do I execute the success method?

        return {
           get : function(itemId) {
             if (isInTestingMode) {
                var promise = $.defer().promise;
                // Mimicking $http.get's success 
                promise.success = function(fn) {
                  promise.then(function() {
                     fn({ itemId : "123", name : "ItemName"}, 200, {}, {});
                  });
                  return promise;
                };
                // Mimicking $http.get's error 
                promise.error = function(fn) {
                   promise.then(null, function(response) {
                     fn("Error", 404, {}, {});
                   });
                   return promise;
                };
                return promise;
             }
             return $http.get("/myapp/items/" + itemId);
           }
        }
whyceewhite
  • 6,317
  • 7
  • 43
  • 51

5 Answers5

18

Just use the deferred method of the $qservice

    var fakeHttpCall = function(isSuccessful) {
    
      var deferred = $q.defer()
    
      if (isSuccessful === true) {
        deferred.resolve("Successfully resolved the fake $http call")
      }
      else {
        deferred.reject("Oh no! Something went terribly wrong in your fake $http call")
      }
      
      return deferred.promise
    }

And then you can call your function like an $http promise (you have to customize whatever you want to put inside of it, of course).

    fakeHttpCall(true).then(
      function (data) {
        // success callback
        console.log(data)
      },
      function (err) {
        // error callback
        console.log(err)
      })
Paul Zaczkowski
  • 2,848
  • 1
  • 25
  • 26
domokun
  • 3,013
  • 3
  • 29
  • 55
  • I'm not understanding how the success or error is forced to execute. I updated my question with an expanded example based on your comment. I don't see how I could force the success method from within the "get" in the MyService. – whyceewhite Jun 24 '14 at 18:39
  • you just need to set the `deferred.resolve(result)` and then return the promise if you want to simulate the success.... or set `deferred.reject(error)` if you want to simulate a failure – KnF Sep 23 '14 at 19:59
  • 4
    I'm pretty sure this won't work as `deferred.promise` doesn't provide `success()` and `error()` for chaining, like $http. See this answer for example for how to achieve that - http://stackoverflow.com/a/19747182/404099. – Ilia Barahovsky Nov 04 '14 at 13:30
  • 2
    @IliaBarahovski `success()` and `error()` are not used anymore. Promises gives you the `then( success, error )` construct, which is more powerful and can be chained to infinite. See https://docs.angularjs.org/api/ng/service/$q – domokun Nov 04 '14 at 21:59
  • @domokun probably I wasn't clear enough. In the answer the second part shows call to `fakeHttpCall(true).success()`. This should fail, since `fakeHttpCall` returns `deferred.promise`. The later indeed defines `then()`, as you mentioned in the comment, but not `success()`. – Ilia Barahovsky Nov 05 '14 at 06:22
  • 1
    @IliaBarahovski you are totally right, I was preaching right but writing wrong :) – domokun Nov 05 '14 at 09:12
6

I found that this post is similar to what I was asking.

However, I wanted a way to mock my service call so that fake data could be returned instead of issuing a true HTTP request call. The best way to handle this situation, for me, is to use angular's $httpBackend service. For example, to bypass a GET request to my "items" resource BUT to not bypass GETs of my partials/templates I would do something like this:

angular
   .module('myApp', ['ngMockE2E'])
   .run(['$httpBackend', function($httpBackend) {
      $httpBackend
        .whenGET(/^partials\/.+/)
        .passThrough();
      $httpBackend
        .whenGET(/^\/myapp\/items\/.+/)
        .respond({itemId : "123", name : "ItemName"});
}]);

See this documentation for more information on $httpBackend.

Community
  • 1
  • 1
whyceewhite
  • 6,317
  • 7
  • 43
  • 51
4

I finally found a way using jasmin. $httpBackend was no option for me, as there were also non-$http-methods I needed mock on the same service. I also think that the controller test needing to specify the url is not perfect as imho the controller and its test should not need to know about it.

Here is how it works:

beforeEach(inject(function ($controller, $rootScope, $q) {
  scope = $rootScope.$new();
  mockSvc = {
    someFn: function () {
    },
    someHttpFn: function () {
    }
  };

  // use jasmin to fake $http promise response
  spyOn(mockSvc, 'someHttpFn').and.callFake(function () {
    return {
      success: function (callback) {
        callback({
         // some fake response
        });
      },
      then: function(callback) {
         callback({
         // some fake response, you probably would want that to be
         // the same as for success 
         });
      },
      error: function(callback){
        callback({
         // some fake response
        });             
      }
    }
  });

  MyCtrl = $controller('MyCtrl', {
    $scope: scope,
    MyActualSvc: mockSvc
  });
}));
hugo der hungrige
  • 12,382
  • 9
  • 57
  • 84
0

You can implement your FakeHttp class:

var FakeHttp = function (promise) {
    this.promise = promise;
    this.onSuccess = function(){};
    this.onError = function(){};
    this.premise.then(this.onSuccess, this.onError);
};
FakeHttp.prototype.success = function (callback) {
    this.onSuccess = callback;
    /**You need this to avoid calling previous tasks**/
    this.promise.$$state.pending = null;
    this.promise.then(this.onSucess, this.onError);
    return this;
};
FakeHttp.prototype.error = function (callback) {
    this.onError = callback;
    /**You need this to avoid calling previous tasks**/
    this.promise.$$state.pending = null;
    this.promise.then(this.onSuccess, this.onError);
    return this;
};

Then in your code, you would return a new fakeHttp out of the promise.

if(testingMode){
    return new FakeHttp(promise);
};

The promise must be asynchronous, otherwise it won't work. For that you can use $timeout.

Gabriel Furstenheim
  • 2,969
  • 30
  • 27
0

easy peasy!

You can do it using angular-mocks-async like so:

var app = ng.module( 'mockApp', [
    'ngMockE2E',
    'ngMockE2EAsync'
]);

app.run( [ '$httpBackend', '$q', function( $httpBackend, $q ) {

    $httpBackend.whenAsync(
        'GET',
        new RegExp( 'http://api.example.com/user/.+$' )
    ).respond( function( method, url, data, config ) {

        var re = /.*\/user\/(\w+)/;
        var userId = parseInt(url.replace(re, '$1'), 10);

        var response = $q.defer();

        setTimeout( function() {

            var data = {
                userId: userId
            };
            response.resolve( [ 200, "mock response", data ] );

        }, 1000 );

        return response.promise;

    });

}]);
Assaf Moldavsky
  • 1,681
  • 1
  • 19
  • 30