angularjsunit-testingangular-mockangularjs-ngmock

How to use ngMock to inject $controller


I'm trying to learn unit testing for Angular using Karma, Jasmine, and ngMock. There are at least 2 places in the Angular docs that show how to write unit tests for a controller, and I just have a couple of questions about how they're doing things.

From the Controller docs, section on testing controllers:

describe('myController function', function() {

  describe('myController', function() {
    var $scope;

    beforeEach(module('myApp'));

    beforeEach(inject(function($rootScope, $controller) {
      $scope = $rootScope.$new();
      $controller('MyController', {$scope: $scope});
    }));

    it("...");
  });
});

Question 1: This one mostly makes sense to me, but here's something I don't quite get. I understand that $controller is grabbing an instance of "MyController", but it looks like what is returned isn't being saved or used anywhere, so the only thing I can think of is that we grab that controller in order to get the correct $scope object? Even that seems like it isn't being saved in a variable anywhere, so I'm still a little confused on how this works behind the scenes. I had the same confusion about how module() works because we seem to be declaring which module we're using, but not saving or using it anywhere else. Is there something behind the scenes that caches the module/controller/scope so we don't have to save it manually or something?


Here's another example from the Unit Testing docs:

describe('PasswordController', function() {
  beforeEach(module('app'));

  var $controller;

  beforeEach(inject(function(_$controller_){
    // The injector unwraps the underscores (_) from around the parameter names when matching
    $controller = _$controller_;
  }));

  describe('$scope.grade', function() {
    it('sets the strength to "strong" if the password length is >8 chars', function() {
      var $scope = {};
      var controller = $controller('PasswordController', { $scope: $scope });
      $scope.password = 'longerthaneightchars';
      $scope.grade();
      expect($scope.strength).toEqual('strong');
    });
  });
});

Question 2: Here in the beforeEach function it's doing the underscore wrapping thing. Is that just so that you can keep the same service name throughout without having to change it?

Question 3: The it() block then uses $controller to do its thing, this time saving what gets returned to var controller but seemingly still never using it beyond this point. So why save it to a variable? The only thing I can think of offhand is that you save it in case you need to use it again inside this it() block, but they just didn't in this example?


I've been looking all over for a good explanation and I can't seem to find one. Sorry if there's a silly explanation I'm missing, but I'm on a time crunch and can't spend any more time spinning my wheels.


Solution

  • 1) Calling module(‘myApp’) will load your module, essentially running the declarations for all your mode functions (providers, directive, controllers) making them available for you to use later. thats how angular finds my Controller when calling $controller(‘myController’).

    As for a reference to the controller returned from $controller, it depends on your implementation. If you are using $scope, Once the controller is instantiated you can reference the functions via $scope.

    in the controller…

    $scope.cancel = function () {
       doStuff();
    };
    

    Then your test can look like this...

    describe(’test Controller', function () {
      var scope;
    
      beforeEach(module(‘myApp'));
    
      beforeEach(inject(function ($rootScope, $controller) {
        scope = $rootScope.$new();
        $controller(‘myController', {
          $scope: scope
        });
      }));
    
      describe(’testing stuff', function () {
        it(’test cancel function', function () {
          scope.cancel();
          expect(...);
        });
      });
    });
    

    If you are using controllerAs syntax and assigning functions to ‘this’ in the controller you can use a reference to the controller in your tests…

    in the controller…

    this.cancel = function () {
       doStuff();
    };
    

    Then your test can look like this...

    describe(’test Controller', function () {
      var scope,
          controller;
    
      beforeEach(module(‘myApp'));
    
      beforeEach(inject(function ($rootScope, $controller) {
        scope = $rootScope.$new();
        controller = $controller(‘myController', {
          $scope: scope
        });
      }));
    
      describe(’testing stuff', function () {
        it(’test cancel function', function () {
          controller.cancel();
          expect(...);
        });
      });
    });
    

    2) Yes, the $controller is a convenience to avoid name collisions;

    3) I think thats a bad example, if they are not using it they shouldn’t need to save it. I prefer to do all my test setup in the beforeEach ().