How to Unit Test Isolated Scope Directive in AngularJS

JavascriptUnit TestingAngularjsJasmineAngularjs Directive

Javascript Problem Overview


What is a good way to unit test isolated scope in AngularJS

JSFiddle showing unit test

Directive snippet

    scope: {name: '=myGreet'},
    link: function (scope, element, attrs) {
        //show the initial state
        greet(element, scope[attrs.myGreet]);
        
        //listen for changes in the model
        scope.$watch(attrs.myGreet, function (name) {
            greet(element, name);
        });
    }

I want to ensure the directive is listening for changes - this does not work with an isolated scope:

    it('should watch for changes in the model', function () {
        var elm;
        //arrange
        spyOn(scope, '$watch');
        //act
        elm = compile(validHTML)(scope);
        //assert
        expect(scope.$watch.callCount).toBe(1);
        expect(scope.$watch).toHaveBeenCalledWith('name', jasmine.any(Function));
    });

UPDATE: I got it to work by checking if the expected watchers were added to the child scope, but it's very brittle and probably using the accessors in an undocumented way (aka subject to change without notice!).

//this is super brittle, is there a better way!?
elm = compile(validHTML)(scope);
expect(elm.scope().$$watchers[0].exp).toBe('name');

UPDATE 2: As I mentioned this is brittle! The idea still works but in newer versions of AngularJS the accessor has changed from scope() to isolateScope():

//this is STILL super brittle, is there a better way!?
elm = compile(validHTML)(scope);                       
expect(elm.isolateScope().$$watchers[0].exp).toBe('name');

Javascript Solutions


Solution 1 - Javascript

See angular element api docs. If you use element.scope() you get the element's scope that you defined in the scope property of your directive. If you use element.isolateScope() you get the entire isolated scope. For example, if your directive looks something like this :

scope : {
 myScopeThingy : '='
},
controller : function($scope){
 $scope.myIsolatedThingy = 'some value';
}

Then calling element.scope() in your test will return

{ myScopeThingy : 'whatever value this is bound to' }

But if you call element.isolateScope() you'll get

{ 
  myScopeThingy : 'whatever value this is bound to', 
  myIsolatedThingy : 'some value'
}

This is true as of angular 1.2.2 or 1.2.3, not sure exactly. In previous versions you had only element.scope().

Solution 2 - Javascript

You can do var isolateScope = myDirectiveElement.scope() to get the isolate scope.

You don't really need to test that $watch was called though.. that's more testing angularjs than testing your app. But I guess it's just an example for the question.

Solution 3 - Javascript

move the logic to a separate controller, ie:

//will get your isolate scope
function MyCtrl($scope)
{
  //non-DOM manipulating ctrl logic here
}
app.controller(MyCtrl);

function MyDirective()
{
  return {
    scope     : {},
    controller: MyCtrl,
    link      : function (scope, element, attrs)
    {
      //moved non-DOM manipulating logic to ctrl
    }
  }
}
app.directive('myDirective', MyDirective);

and test latter as you would any controller - passing the scope object in directly (see Controllers section here for an example).

if you need to trigger $watch in your test do:

describe('MyCtrl test', function ()
{
  var $rootScope, $controller, $scope;

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

    $scope = $rootScope.$new({});
    $scope.foo = {x: 1}; //initial scope state as desired
    $controller(MyCtrl, {$scope: $scope}); //or by name as 'MyCtrl'
  });

  it('test scope property altered on $digest', function ()
  {
    $scope.$digest(); //trigger $watch
    expect($scope.foo.x).toEqual(1); //or whatever
  });
});

Solution 4 - Javascript

I'm not sure it's possible with isolate scope (although I hope someone proves me wrong). The isolate scope that gets created in the directive is, well, isolated, so the $watch method in the directive is different from the scope that you're spying on in the unit test. If you change scope: {} to scope: true, the directive scope will inherit prototypically and your tests should pass.

I guess this isn't the most ideal solution, because sometimes (a lot of the time), isolate scope is a good thing.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSavoryBytesView Question on Stackoverflow
Solution 1 - JavascriptYair TavorView Answer on Stackoverflow
Solution 2 - JavascriptAndrew JoslinView Answer on Stackoverflow
Solution 3 - JavascriptNikitaView Answer on Stackoverflow
Solution 4 - JavascriptUnicodeSnowmanView Answer on Stackoverflow