How to write testable controllers with private methods in AngularJs?

Alright, so I have been stumbling upon some issue for a long time and I would like to hear an opinion from the rest of community.

First, let's look at some abstract controller.

function Ctrl($scope, anyService) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      util();
   };

   function util() {
      anyService.doSmth();
   }

}

Clearly we have here:

  • regular scaffold for controller with $scope and some service injected
  • some field and function attached to the scope
  • private method util()

Now, I'd like to cover this class in unit tests (Jasmine). However, the problem is that I want to verify that when I click (call whenClicked()) some item that the util() method will be called. I don't know how to do that, since in Jasmine tests I'm always getting errors that either the mock for util() hasn't been defined or was not called.

Note: I'm not trying to fix this particular example, I'm asking about testing such code pattern in general. So please don't tell me "what is exact error". I'm asking how to do that, not how to fix this.

I have been trying a number of ways around this:

  • obviously I cannot use $scope in my unit tests as I don't have this function attached to this object (it usually ends with message Expected spy but got undefined or similar)
  • I tried attaching those functions to the controller object via Ctrl.util = util; and then verifying mocks like Ctrl.util = jasmine.createSpy() but in this case Ctrl.util is not being called so tests fail
  • I tried to change util() to be attached to this object and mocking Ctrl.util again, with no luck

Well, I cannot find my way around this, I would expect some help from JS ninjas, a working fiddle would be perfect.


Solution 1:

The controller function you provided will be used by Angular as a constructor; at some point it will be called with new to create the actual controller instance. If you really need to have functions in your controller object that are not exposed to the $scope but are available for spying/stubbing/mocking you could attach them to this.

function Ctrl($scope, anyService) {

  $scope.field = "field";
  $scope.whenClicked = function() {
    util();
  };

  this.util = function() {
    anyService.doSmth();
  }
}

When you now call var ctrl = new Ctrl(...) or use the Angular $controller service to retrieve the Ctrl instance, the object returned will contain the util function.

You can see this approach here: http://jsfiddle.net/yianisn/8P9Mv/

Solution 2:

Namespacing it on the scope is pollution. What you want to do is extract that logic into a separate function which is then injected into your Controller. i.e.

function Ctrl($scope, util) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      util();
   };
}

angular.module("foo", [])
       .service("anyService", function(...){...})
       .factory("util", function(anyService) {
              return function() {
                     anyService.doSmth();
              };
       });

Now you can unit test with mocks your Ctrl as well as "util".

Solution 3:

I'm going to chime in with a different approach. You shouldn't be testing private methods. That's why they are private - it's an implementation detail that is irrelevant for the usage.

For example, what if you realize that util was used in several places but now, based on other code refactoring, it's only called in this one place. Why have an extra function call? Just include anyService.doSmith() inside you $scope.whenClicked() With the suggestions above, assuming you are testing that util() is called, your tests will break even though you haven't changed the functionality of the program. One of the main values of unit testing is to simplify refactoring without breaking things, so if you didn't break things, the test shouldn't fail.

What you need to do is ensure that when $scope.whenClicked is called, anyService.doSmth() is also called. You just need:

spyOn(anyService,'doSmith')
scope.whenClicked();
expect(anyService.doSmith).toHaveBeenCalled();

Solution 4:

I'm adding an answer containing my current approach, hoping to get some comments and perhaps sparkle discussion about whether or not this is a good solution.

We are attaching private functions to the controller function (thus making them public, which enables mocking). To avoid having to repeat controller name all the times and making syntax more appealing, we are creating self object which holds reference to controller function. So it becomes:

function Ctrl($scope, anyService) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      self.util();
   };

   var self = Ctrl; // For the sake of syntax simplicity only

   self.util = function() {
      anyService.doSmth();
   };

}

and then in unit tests now we can use:

Ctrl.util = jasmine.createSpy("util()");
expect(Ctrl.util).toHaveBeenCalled();

I still don't like this very much, but I think this is the simplest way of doing this. I'm hoping someone will find better approach.