UI-router interfers with $httpbackend unit test, angular js


This is a controller with a submit function:

$scope.submit = function(){   

 $http.post('/api/project', $scope.project)
      .success(function(data, status){
        $modalInstance.dismiss(true);
      })
      .error(function(data){
        console.log(data);
      })
  }
}

This is my test

it('should make a post to /api/project on submit and close the modal on success', function() {
    scope.submit();

    $httpBackend.expectPOST('/api/project').respond(200, 'test');

    $httpBackend.flush();

    expect(modalInstance.dismiss).toHaveBeenCalledWith(true);
  });

The error I get is:

Error: Unexpected request: GET views/appBar.html

views/appBar.html is my templateUrl:

 .state('project', {
    url: '/',
    templateUrl:'views/appBar.html',
    controller: 'ProjectsCtrl'
  })

So somehow ui-router is making my $httpBackend point to this instead of my submit function. I have the same issue in all my tests using $httpBackend.

Is there any solution to this?

Take this gist https://gist.github.com/wilsonwc/8358542

angular.module('stateMock',[]);
angular.module('stateMock').service("$state", function($q){
    this.expectedTransitions = [];
    this.transitionTo = function(stateName){
        if(this.expectedTransitions.length > 0){
            var expectedState = this.expectedTransitions.shift();
            if(expectedState !== stateName){
                throw Error("Expected transition to state: " + expectedState + " but transitioned to " + stateName );
            }
        }else{
            throw Error("No more transitions were expected! Tried to transition to "+ stateName );
        }
        console.log("Mock transition to: " + stateName);
        var deferred = $q.defer();
        var promise = deferred.promise;
        deferred.resolve();
        return promise;
    }
    this.go = this.transitionTo;
    this.expectTransitionTo = function(stateName){
        this.expectedTransitions.push(stateName);
    }

    this.ensureAllTransitionsHappened = function(){
        if(this.expectedTransitions.length > 0){
            throw Error("Not all transitions happened!");
        }
    }
});

Add it to a file called stateMock in your test/mock folder, include that file in your karma config if it isn't already picked up.

The setup before your test should then look something like this:

beforeEach(module('stateMock'));

// Initialize the controller and a mock scope
beforeEach(inject(function ($state //other vars as needed) {
    state = $state;
    //initialize other stuff
}

Then in your test you should add

state.expectTransitionTo('project');

This Github issue about Unit Testing UI Router explains more fully what's happening.

The problem is that $httpBackend.flush() triggers a broadcast which then triggers the otherwise case of the stateProvider.

A simple solution can be to do the following setup, as mentionned by @darinclark in Github thread mentionned above. This is valid if you do not need to test state transitions. Otherwise have a look to @rosswil's answer that is inspired by @Vratislav answer on Github.

beforeEach(module(function ($urlRouterProvider) {
    $urlRouterProvider.otherwise(function(){return false;});
}));

EDITED

Thanks to Chris T to report this in the comments, seems after v0.2.14? the best way to do this is to use

beforeEach(module(function($urlRouterProvider) {
  $urlRouterProvider.deferIntercept();
}));

I have the same error your commented, after a call service they ask me about the url of otherwise ui-route.

To solve the problem of call the otherwise ui-route in testing is not inject $state in beforeach statment. In my testing $state not have sense to use it.


Move your services to their own module that have no dependency on ui.router. have your main app depend on this module. When you test don’t test the main app, test the module that has your services in it. The stateprovider won’t try to change state/route because this module knows nothing about the ui.router. This worked for me.


If you don't want to add gist files like it says in the correct solution you can add a "when" condition to your $httpBackend to ignore GET petitions of views like this:

$httpBackend.when("GET", function (url) {
    // This condition works for my needs, but maybe you need to improve it
    return url.indexOf(".tpl.html") !== -1;
}).passThrough();