Search code examples
angularjsunit-testingscopeangular-mockangularjs-ngmock

ngMock injecting $scope local into controller


This is a continuation of another question I asked that was successfully answered.

I'm learning how to unit test AngularJS applications with Karma, Jasmine, and ngMock. Here's the code I have a question about:

describe('myController function', function() {

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

    beforeEach(module('myApp'));

    beforeEach(inject(function($rootScope, $controller) {

      // These are the 2 lines I'm a bit confused about:
      scope = $rootScope.$new();
      $controller('MyController', {$scope: scope});

    }));

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

Question 1: Why do we create a new scope and include it in the locals injection area in this line: $controller('MyController', {$scope: scope});? It seems to work just fine (as in, scope now represents the $scope object from that controller and has all the properties and functions it's supposed to), but the code seems to imply that we're resetting the controller's $scope with our newly-created (and empty) scope (from the line scope = $rootScope.$new();). So I think I'm just not fully understanding the purpose/inner-workings of those locals.

Question 2: I also have seen from my searching that new scope gets created in 2 common ways, either the way shown above with $rootScope.$new(), or by simply declaring scope = {}. Even the Angular docs do it both ways, as seen here (using $rootScope.$new()) and here (using $scope = {}. Why do this one way over another? Is there a difference?


Solution

  • Why do we create a new scope

    There are a couple of reasons that come to mind, but the short answer is that you don't have to if you don't want to. You can just as easily pass in $rootScope and your tests will still work. It is also just typically done because it is more in line with what actually happens in angular - the $scope that the controller is injected with is likely a descendent of $rootScope.

    What is $controller('MyController', {$scope: scope}); doing?

    The ngMock module provides the $controller function as a sort of constructor for creating your controller - well, technically it's a decorator to the $controller function in the ng module, but that's not important. The first argument is typically a string, but can be a function.

    If called with a function then it's considered to be the controller constructor function. Otherwise it's considered to be a string which is used to retrieve the controller constructor...

    The second argument are the "locals" to be injected into the controller during creation.

    So, in your example, you are saying you want to create the "MyController" controller, and you want to inject your scope variable as the argument named $scope in the controllers function.

    The whole point of this is to inject your own version of the scope into the controller, but a version that you created in your test, so that you can assert the different things that happen to the scope because of the controller.

    This is one of the benefits of dependency injection.

    Examples

    If the following makes sense:

    var scope = {};
    scope.add = function(){};
    expect(typeof scope.add).toBe('function');
    

    then let's take it one step further and move the adding of the function into another function:

    var addFunctionToScope = function(scope) {
      scope.add = function(){};
    };
    var scope = {};
    addFunctionToScope(scope);
    expect(typeof scope.add).toBe('function');
    

    Now just pretend your new function to add the function to the scope is really just called $controller, instead of "addFunctionToScope".

    var scope = {};
    $controller('SomeController', {$scope: scope});
    expect(typeof scope.add).toBe('function');
    

    Why use $rootScope.$new() over {}

    Again, you can use either. However, if you plan on using some of the scope specific functions, like $on, or $new, or $watch, etc - you will want to inject an actual scope object, created using the $new function on an existing scope.

    Docs