3

I'm trying to mock a response to a JSONP GET request which is made with a function that returns an ES6 promise which I've wrapped in $q.when(). The code itself works just fine, however, in the unit tests the request is not being caught by $httpBackend and goes through right to the actual URL. Thus when flush() is called I get an error stating Error: No pending request to flush !. The JSONP request is made via jQuery's $.getJSON() inside the ES6 promise so I opted to try and catch all outgoing requests by providing a regex instead of a hard-coded URL.

I've been searching all over trying to figure this out for a while now and still have yet to understand what's causing the call to go through. I feel as if the HTTP request in the ES6 promise is being made "outside of Angular" so $httpBackend doesn't know about it / isn't able to catch it, although that may not be the case if the call was being made inside of a $q promise from the get-go. Can anyone possibly tell me why this call is going through and why a simple timeout will work just fine? I've tried all combinations of $scope.$apply, $scope.$digest, and $httpBackend.flush() here, but to no avail.

Maybe some code will explain it better...

Controller

function homeController() {
    ...
    var self = this;
    self.getData = function getData() {
        $q.when(user.getUserInformation()).then(function() {
            self.username = user.username;
        });
    };
}

Unit Test

...

beforeEach(module('home'));

describe('Controller', function() {
    var $httpBackend, scope, ctrl;

    beforeEach(inject(function(_$httpBackend_, $rootScope, $componentController) {
        $httpBackend = _$httpBackend_;
        scope = $rootScope.$new(); // used to try and call $digest or $apply
        // have also tried whenGET, when('GET', ..), etc...
        $httpBackend.whenJSONP(/.*/)
                    .respond([
                        {
                            "user_information": {
                                "username": "TestUser",
                            }
                        }
                    ]);
        ctrl = $componentController("home");
    }));

    it("should add the username to the controller", function() {
        ctrl.getData(); // make HTTP request
        $httpBackend.flush(); // Error: No pending request to flush !
        expect(ctrl.username).toBe("TestUser");
    });
});

...

For some reason this works, however:

    it("should add the username to the controller", function() {
        ctrl.getData(); // make HTTP request
        setTimeout(() => {
            // don't even need to call flush, $digest, or $apply...?
            expect(ctrl.username).toBe("TestUser");
        });
    });
stevenelberger
  • 1,368
  • 1
  • 10
  • 19
  • 1
    httpBackend is a mock implementation which is injected into the $http service by angular for the unit tests, if you aren't using the $http service to make a request, you won't be able to use the httpBackend to catch it – Graham S Jul 29 '16 at 03:32

1 Answers1

0

Thanks to Graham's comment, I was brought further down a different rabbit hole due to my lack of understanding several things which I will summarize here in case someone ends up in the same situation...

  1. I didn't fully understand how JSONP works. It doesn't rely on XmlHttpRequest at all (see here). Rather than trying to fiddle with mocking responses to these requests through JSONP I simply switched the "debug" flag on the code I was using which disabled JSONP so the calls were then being made via XHR objects (this would fail the same origin policy if real responses were needed from this external API).
  2. Instead of trying to use jasmine-ajax, I simply set a spy on jQuery's getJSON and returned a mock response. This finally sent the mocked response to the ES6 promise, but for some reason the then function of the $q promise object which resulted from wrapping the ES6 promise wasn't being called (nor any other error-handling functions, even finally). I also tried calling $scope.$apply() pretty much anywhere in the off chance it would help, but to no avail.

    Basic implementation (in unit test):

     ...
     spyOn($, 'getJSON').and.callFake(function (url, success) {
         success({"username": "TestUser"}); // send mock data
     });
     ctrl.getData(); // make GET request
     ...
    

    Problem (in controller's source):

     // user.getUserInformation() returns an ES6 promise
     $q.when(user.getUserInformation()).then(function() {
         // this was never being called / reached! (in the unit tests)
     });
    
  3. Ultimately I used #2's implementation to send the data and just wrapped the assertions in the unit test inside of a timeout with no time duration specified. I realize that's not optimal and hopefully isn't how it should be done, but after trying for many hours I've about reached my limit and given up. If anyone has any idea as to how to improve upon this, or why then isn't being called, I would honestly love to hear it.

    Unit Test:

     ...
     ctrl.getData(); // make GET request
     setTimeout(() => {
         expect(ctrl.username).toBe("TestUser"); // works!
     });
    
Community
  • 1
  • 1
stevenelberger
  • 1,368
  • 1
  • 10
  • 19