Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • We mock out the API request so that GET requests to /api/users will return an array of 3 objects.
  • We go to the state named users
  • We expect to see that the $scope.users should be an array of 3 objects.
  • From this test, we tested both the resolves defined in the router, and that the controller assigned the resolved objects correctly.

Thanks Bill

like image 948
Bill Avatar asked Jul 16 '14 14:07

Bill


1 Answers

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.

like image 83
scarlz Avatar answered Sep 24 '22 08:09

scarlz