1

My ng app is working fine, but I am trying to write a ngMock test for my controller; I am basically following along the example on angular's website: https://docs.angularjs.org/api/ngMock/service/$httpBackend

The problem I am running into is that it complains about unexpected request even when request is being expected.

PhantomJS 1.9.8 (Windows 8 0.0.0) NotificationsController should fetch notification list FAILED

Error: Unexpected request: GET Not valid for testsapi/AspNetController/AspNetAction Expected GET api/AspNetController/AspNetAction

What I do not get is that, on the error line, why is there a "tests" word appended before my service url? I thought it should be sending to 'api/AspNetController/AspNetAction' What am I doing wrong here. I can't find any one else running into the same problem as me through google.

Edit: I noticed that, if i remove the sendRequest portion from my controller, and have the unit test log my request object in console, i see the following json.

{  
   "method":"GET",
   "url":"Not valid for testsapi/AspNetController/AspNetAction",
   "headers":{  
      "Content-Type":"application/json"
   }
}

here is the controller code

angular.module('MainModule')
    .controller('NotificationsController', ['$scope', '$location', '$timeout', 'dataService',
        function ($scope, $location, $timeout, dataService) {
            //createRequest returns a request object
            var fetchNotificationsRequest = dataService.createRequest('GET', 'api/AspNetController/AspNetAction', null);
            //sendRequest sends the request object using $http
            var fetchNotificationsPromise = dataService.sendRequest(fetchNotificationsRequest);
            fetchNotificationsPromise.then(function (data) {
                //do something with data.
            }, function (error) {
                alert("Unable to fetch notifications.");
            });
    }]
);

Test code

describe('NotificationsController', function () {
    beforeEach(module('MainModule'));
    beforeEach(module('DataModule')); //for data service

    var $httpBackend, $scope, $location, $timeout, dataService;

    beforeEach(inject(function ($injector) {

        $httpBackend = $injector.get('$httpBackend');

        $scope = $injector.get('$rootScope');
        $location = $injector.get('$location');
        $timeout = $injector.get('$timeout');
        dataService = $injector.get('dataService');

        var $controller = $injector.get('$controller');

        createController = function () {
            return $controller('NotificationsController', {
                '$scope': $scope,
                '$location': $location,
                '$timeout': $timeout,
                'dataService': dataService,
            });
        };
    }));

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

    it('should fetch notification list', function () {
        $httpBackend.expectGET('api/AspNetController/AspNetAction');        //this is where things go wrong
        var controller = createController();

        $httpBackend.flush();
    });

});

Data service code

    service.createRequest = function(method, service, data) {
        var req = {
            method: method, //GET or POST
            url: someInjectedConstant.baseUrl + service,
            headers: {
                'Content-Type': 'application/json'
            }
        }

        if (data != null) {
            req.data = data;
        }

        return req;
    }

    service.sendRequest = function (req) {
        return $q(function (resolve, reject) {
            $http(req).then(function successCallback(response) {
                console.info("Incoming response: " + req.url);
                console.info("Status: " + response.status);
                console.info(JSON.stringify(response));

                if (response.status >= 200 && response.status < 300) {
                    resolve(response.data);
                } else {
                    reject(response);
                }
            }, function failCallback(response) {
                console.info("Incoming response: " + req.url);
                console.info("Error Status: " + response.status);
                console.info(JSON.stringify(response));

                reject(response);
            });
        });
    }

ANSWER:

since dataService created the finalized webapi url by someInjectedConstant.baseUrl + whatever_relative_url passed in from controller, In the test that I am writting, I will have to inject someInjectedConstant and

$httpBackend.expectGET(someInjectedConstant.baseUrl + relativeUrl)

instead of just doing a $httpBackend.expectGET(relativeUrl)

Ji He
  • 13
  • 4
  • please could you post your dataservice and the asp routehandler code? It's most likely that 'tests' is getting prepended in one of those – br3w5 Nov 23 '15 at 18:12
  • @br3w5, I have appended code for dataservice, and the asp route is just standard; hitting http://localhost/api/AspNetController/AspNetAction will display the json data I need. At this stage, no authentication is used. – Ji He Nov 23 '15 at 18:19
  • i've had similar issues when changing an endpoint but that was clearer and caught by existing tests...shouldn't you be making a `GET` to `/api/AspNetController/AspNetAction` (note the first slash)? – br3w5 Nov 23 '15 at 18:26
  • @br3w5 well, the data service is appending that slash together with the domain (localhost). The funny thing is that the app works completely fine, but the unit test is complaining. I was initially using jasmine spy, who has no problem resolving mocked data for that endpoint, but i switched ngMock due to scope related problems. – Ji He Nov 23 '15 at 18:33
  • @br3w5, I just made an edit to my post. If i do not send the $http request, and attempt to log the request object it self. I noticed that the url in the request object is wrong at creation time. – Ji He Nov 23 '15 at 18:45
  • ok but you don't want or need to hardcode the domain into your `$http` request you can just use '/api/AspNetController/AspNetAction` as the url – br3w5 Nov 23 '15 at 20:10
  • also you don't need to wrap `$http` in `$q` - `$http` already returns a promise – br3w5 Nov 23 '15 at 20:11
  • can you show how you are logging the request object? and can you put a log just before returning the request object in dataservice? `console.log(angular.toJson(req), true)` should do it – br3w5 Nov 23 '15 at 20:13
  • @br3w5, I have found out the problem and have appended the answer to the end of my post. it is indeed along the lines of your first comment. That was what i thought at first. But i only found out that the baseUrl that another developer has chosen was generically named "GET Not valid for tests", and i thought it was some framework error msg. Thank you for your time. Please do up a post that is similar to my updated answer and I'll mark it as the answer to my problem. – Ji He Nov 23 '15 at 20:43
  • @br3w5 just incase you are wondering why it took so much effort to find such an easy bug. I was using jasmine spy for resolving http promises, and relative url always worked fine for me. But after switching to ngMock, it needs full url. But our gulp clever choses "someInjectedConstant" between deployment/local/test environments, and this is the first time I need to worry about "test" environment (because of the need for full url), plus the coincidence of a generically named base url; I was mislead into thinking there might be some framework related issues. – Ji He Nov 23 '15 at 20:53
  • ok glad you found it :) – br3w5 Nov 23 '15 at 21:02

1 Answers1

0

Clearly Not valid for tests is getting prepended to your url somewhere in your code. It's also not adding the hardcoded domain (see note below). Check through all your code and any other parts of the test pipeline that might be adding this to the url.

A couple of points on your code:

  • avoid hardcoding domain names in your code (I see you've fixed this in your updated answer)
  • maybe someInjectedConstant could be more explicitly named
  • there is no need for you to wrap $http with $q, so service.sendRequest can be:

    service.sendRequest = function (req) {
        $http(req).then(function (response) { // no need to name the function unless you want to call another function with all success/error code in defined elsewhere
            console.info("Incoming response: " + req.url);
            console.info("Status: " + response.status);
            console.info(JSON.stringify(response));
            return response.data; // angular treats only 2xx codes as success
        }, function(error) {
            console.info("Incoming response: " + req.url);
            console.info("Error Status: " + response.status);
            console.info(JSON.stringify(response));
        });
    }
    
br3w5
  • 4,403
  • 5
  • 33
  • 42