2

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?

Community
  • 1
  • 1
bobbyz
  • 4,946
  • 3
  • 31
  • 42

1 Answers1

3

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

Seth Flowers
  • 8,990
  • 2
  • 29
  • 42
  • The `$rootScope.$new()` vs. `{}` makes sense, thanks for that! I still don't understand why we create a new, empty scope, but it somehow has all the methods and properties from our controller. Do we instantiate a property called `$scope` and make it an empty object literal (or a new `$scope` object) and *then* `$contoller` fills it up with the properties from the actual Angular Controller? I just thought of that possible explanation, and that would make sense to me. Otherwise I'm still a little lost on that part. :/ – bobbyz Jan 05 '16 at 21:58
  • I think you might be confusing scope and controller. The scope is typically injected as a field into the controller. The controller can then modify the scope by adding/removing/updating fields on it. The scope basically acts as the glue between the controller and the template (think view). Typically, you will have tests that check each thing the controller does. So if your controller immediately does something like `$scope.assigned = true;`, you would have a test that checks that $scope.assigned is true after you create the controller. – Seth Flowers Jan 05 '16 at 22:07
  • I understand the difference between the scope and controller and how it plays with the view (I think, at least... ha), but what I'm confused about is how we can create a blank `$scope` object, assign that blank `$scope` object to the controller we're testing, but then the blank `$scope` object suddenly has all the properties and methods from the actual controller. I'm assuming I'm still thinking about the process all wrong, but that's where I'm still stuck right now... Adding the locals object is confusing me :( – bobbyz Jan 05 '16 at 23:26
  • Updated the answer to explain what the $controller function is doing with the locals. – Seth Flowers Jan 06 '16 at 14:13
  • So when I inject my `scope` variable as the argument called `$scope`, but my newly-created `scope` variable is an empty scope, how can I suddenly have access to something like `scope.add()` or `scope.x` or what have you? I _just_ set `scope` to a new, empty childScope, but somehow it's getting all the properties and methods from my controller, despite me telling the test "here, use this blank, empty scope object as your `$scope`". I'm guessing it has something to do with inheritance, but this is where I'm still confused. Sorry my mind is having so much difficulty understanding this :/ – bobbyz Jan 06 '16 at 16:30
  • Your controller must be adding it to the scope, which is normal. Think about it this way - let's say your controller sets a function called "add" on the scope - if I create an empty scope, and then give it to the controller, and the controller sets an "add" function on it, I am still referencing the same scope, so now I can "see" the "add" function on the scope, which means I can test it. This is totally expected. If this sounds confusing, you might want to read about the differences between reference types and value types. – Seth Flowers Jan 06 '16 at 16:43
  • Ohhhhh oh oh oh! So I'm not _setting_ the `$scope` to an empty scope object, I'm _handing_ the controller a fresh instance of a scope to then run through the controller's code and add all the properties and methods to it. It's so simple, I was just thinking of the order the code executes in the wrong way. Thank you so much! Could you add that little tidbit to your answer (That you're injecting a new instance of a childScope, and _then_ the controller goes through its code to add properties)? I'll mark it as the accepted answer then :) – bobbyz Jan 06 '16 at 16:48
  • Exactly - I've updated the answer to step through some simple examples to highlight this fact. – Seth Flowers Jan 06 '16 at 16:51