1

Trying to test an angular service that returns an $http GET request and the then handler, but I'm not able to test that the logic actually works inside of the then function. Here is a basic, truncated version of the service code:

angular.module('app').factory('User', function ($http) {
  var User = {};

  User.get = function(id) {
    return $http.get('/api/users/' + id).then(function (response) {
      var user = response.data;
      user.customProperty = true;  
      return user;
    });
  };

  return User;
});

And here is the test:

beforeEach(module('app'));
beforeEach(inject(function(_User_, _$q_, _$httpBackend_, _$rootScope_) {
  $q = _$q_;
  User = _User_;
  $httpBackend = _$httpBackend_;
  $scope = _$rootScope_.$new();
}));

afterEach(function () {
  $httpBackend.verifyNoOutstandingRequest();
  $httpBackend.verifyNoOutstandingExpectation();
});

describe('User factory', function () {

  it('gets a user and updates customProperty', function () {
    $httpBackend.expectGET('/api/users/123').respond({ id: 123 });
    User.get(123).then(function (user) {
      expect(user.customProperty).toBe(true);  // this never runs
    });

    $httpBackend.flush();
  });

});

I feel like I've tried pretty much everything to test the logic in the then call, so if someone can offer suggestions I would greatly appreciate it.

Edit: my problem was also due to nonstandard injection practices, so the answer below worked outside of that.

3
  • If you are doing this in a service, then you wouldn't have scope to apply. So, I always put the promise chain stuff in my controllers and test it there. Here is a link to a similar question stackoverflow.com/a/24407839/2740086 Commented Aug 4, 2014 at 19:53
  • Thanks for that. Yeah I am able to pass in a fake promise when I test the use of the factory in, say, a controller, but I want to test that the logic inside of the factory promise is sound. Is that just bad practice? Commented Aug 4, 2014 at 19:57
  • What I do is this: I separate the promise stuff in the controller, so I can control it with promise.resolve() then $scope.apply(). I then pass a success or failure object to a "resolver" service that actually does what I want with that success or failure. Basically the service has a getResourceFromServer() method and a resolveGetResourceFromServer(objectFromServer, successBool) method that I use to do whatever in the service. I don't know if it is "best practice" but I find it makes testing a little easier. Commented Aug 4, 2014 at 20:01

1 Answer 1

4

A few things need to be changed

  • Use whenGET instead of expectGET in order to fake a response
  • In the test then callback, set the response to a variable available outside the callback so you can test it in an expect call
  • Make sure the expect call is outside any callbacks, so it always runs and shows any failures.

Putting it all together:

it('gets a user and updates customProperty', function () {
  $httpBackend.whenGET('/api/users/123').respond({ id: 123 });
  User.get(123).then(function(response) {
    user = response;
  })
  $httpBackend.flush();
  expect(user.customProperty).toBe(true); 
});

As can be seen at http://plnkr.co/edit/9zON7ALKnrKpcOVrBupQ?p=preview

Sign up to request clarification or add additional context in comments.

4 Comments

You can also use the jasmine spyOn and andCallFake tools, to return a promise with the state you want, but, in order to do that, you must have to separate the loading business into a dedicated service. A service can be mocked, and then the spyOn become usable. If you want to mock the $http without using the $httpBackend, you can mock it throught a module(function($provide) { $provide.value('$http', yourBadMockBecauseYouAreLazy ); })
Could it have something to do with how we're injecting $http then? Because I still get "No pending requests to flush" when I try that. We call it like var $http = angular.injector(['dn']).get('$http'); to have our resources extend a base Resource type.
@Tom It sounds like the code you posted might not be representative of your actual situation. As there is now an answer to this question (i.e. mine!) I suggest posting a new question with code that shows your issue, with a link to a Plunker showing it work (or rather, not work).
Sorry for the confusion with all this, your fix did work once I changed how the service was defined. Has to do with the $injector possibly returning a new instance of $http, but that needs a separate investigation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.