Bill
Bill

Reputation: 3209

Karma Testing with Angular.js + UI Router

I was wondering how should one test with Karma test with Angular.js + UI router?

I have the following state defined: Which has two resolves that fetches some data and prepares the data for the controller. (Coming from Ember background, this makes a lot of sense.)

$stateProvider
  .state('users', {
    resolve: {
      getData: function (User) {
        return User.query().$promise
      },
      stateModels: function (getData) {
        var models = {}
        models.users = getData
        return models
      }
    },
    url: '/',
    templateUrl: '/views/users/index.html',
    controller: 'UsersIndexCtrl'
  })

Our UserIndexCtrl looks like: (Which takes the resolved stateModels and assigns it to the $scope, so the view can use it)

app.controller('UsersIndexCtrl', [
  '$scope', '$state', 'stateModels',
  function ($scope, $state, stateModels) {

    $scope.users = stateModels.users

  }])

This is working great in the browser, I am seeing the right results. However, when it comes to testing it is giving me odd errors.

Here is an example Karma unit test:

describe('controllers', function () {

  var $httpBackend
    , $rootScope
    , $scope
    , $state
    , $httpBackend
    , $controller

  beforeEach(module('app'))

  beforeEach(inject(function ($injector) {
    $state = $injector.get('$state')
    $rootScope = $injector.get('$rootScope')
    $httpBackend = $injector.get('$httpBackend')
    $scope = $rootScope.$new()
    $controller = $injector.get('$controller')
  }))

  it('UserIndexCtrl should exist', inject(function () {
    $httpBackend
      .expect('GET', '/api/users')
      .respond(200, {users: [ {}, {}, {} ]})

    $state.go('users')
    $rootScope.$apply()

    $controller('AdminZonesIndexCtrl', { $scope: $scope, $state: $state });
    $rootScope.$apply()
    assert.equal($scope.users.length, 3)
  }))

})

And I am seeing:

[$injector:unpr] Unknown provider: stateModelsProvider <- stateModels
http://errors.angularjs.org/1.3.0-build.2937+sha.4adc44a/$injector/unpr?p0=stateModelsProvider%20%3C-%20stateModels
Error: [$injector:unpr] Unknown provider: stateModelsProvider <- stateModels
http://errors.angularjs.org/1.3.0-build.2937+sha.4adc44a/$injector/unpr?p0=stateModelsProvider%20%3C-%20stateModels

The idea here is:

Thanks Bill

Upvotes: 14

Views: 8376

Answers (1)

scarlz
scarlz

Reputation: 2512

The reason for your error is that you are first transitioning to a state which instantiates your UsersIndexCtrl with a new scope, but then subsequently creating another instance of the controller (again, with a new scope) within the test. The two are independent of one another, and in the second instance, stateModels is an unknown/unavailable dependency.

So, while your ideas are valid testing concerns, in trying to assert all three together you are essentially performing an end-to-end test in a unit test environment. You should not want to - doing so will create a brittle dependency on:

  • the controller belonging to the "users" state
  • that somewhere in the state transition a particular http request is called
  • that the stateModels dependency is bound to the scope.

Of these assertions, only the last is of any concern to the controller. Unit tests for your controller don't care how/when it was instantiated or where the stateModels dependency came from, they are only concerned with the controller's own behaviour. So, let's split this behaviour up:

Unit testing the controller

Your first test should be reduced to the following:

it('binds the users to the scope', function(){
   var stateModels = [{}, {}, {}];
   $controller('UserIndexCtrl', {$scope: $scope, stateModels: stateModels});
   assert.equal($scope.users, stateModels);
});

Note that as you add more tests, you will likely want to move your controller instantiation to a beforeEach block.

Testing the route

The concern of testing a route is really that of application behaviour, for which you should defer to Protractor. However, if you particularly want to perform a unit test on a state, this is perhaps best served by testing the configuration of the state itself. For example:

it('resolves the stateModels dependency', function() {
   var state = $state.get('users');
   assert.isDefined(state.resolve.stateModels); 
   // perform assertion that stateModels function resolves to what is expected
   // Note: any such assertion should stub any dependency being used, to ensure
   // we are testing in isolation.
});

That notwithstanding, I personally don't elect to unit test routing/route configuration, and instead acquire such coverage through e2e testing with Protractor.

Upvotes: 26

Related Questions