How to test Angular 1.5 components?

TL;DR: We can test components as we were used to test directives.

Yes, you heard all-right, there is a new kid in town and last Friday Angular 1.5 was released bringing us, among other updates, the component sugar. If you want to know more about it Todd Motto explored the component method back in November when it was still beta.

Why components? The web is shifting towards web components and on the way we are adopting component based architectures. Angular 2 will be all about components and to ease the migration process we better start preparing now. If you haven’t started yet I recommend you to read thoroughly Refactoring Angular Apps to Component Style by Tero Parviainen and Preparing for Angular 2 by Juri Strumpflohner.

Unit testing components

The good news are that we can test components as we were used to test directives, because component is nothing but a helper method to create directives with some sensible defaults. Therefore if we are just switching the syntax from directive to component all our tests should keep passing.

The big difference with directives is that now we pass a definition object rather than a function returning an object. So our component can be as simple as follows.

angular.module('myComponentModule', [])
  .component('myComponent', {
    bindings: {
      myBinding: '@'
    },
    controller: function() {
      this.myTitle = 'Unit Testing AngularJS';
    },
    template: '
<h1>{{ $ctrl.myTitle }} {{ $ctrl.myBinding }}</h1>
'
  });

By default the component controller is named $ctrl and the bindings are passed from the parent scope to our component:

<my-component my-binding="1.5"></my-component>

So we can compile the element and test that it renders what we expected.

describe('Component: myComponent', function () {
  beforeEach(module('myComponentModule'));

  var element;
  var scope;
  beforeEach(inject(function($rootScope, $compile){
    scope = $rootScope.$new();
    element = angular.element('<my-component my-binding="{{outside}}"></my-component>');
    element = $compile(element)(scope);
    scope.outside = '1.5';
    scope.$apply();
  }));

  it('should render the text', function() {
    var h1 = element.find('h1');
    expect(h1.text()).toBe('Unit Testing AngularJS 1.5');
  });

});

Testing the component controller

To test an instance of the component controller we can grab it from the compiled element.

var controller;
beforeEach(function() {
  controller = element.controller('myComponent');
});

And test what the controller is exposing to the view.

it('should expose my title', function() {
  expect(controller.myTitle).toBeDefined();
  expect(controller.myTitle).toBe('Unit Testing AngularJS');
});

That also includes the external bindings.

it('should have my binding bound', function() {
  expect(controller.myBinding).toBeDefined();
  expect(controller.myBinding).toBe('1.5');
});

There is though a new addition for Angular 1.5 and the ngMock module has been updated with the $componentController method.

This help us to test the component controller when we don’t want or need to test the compiled element. So we can create an instance of the controller by just passing the scope and the bindings.

var controller;
var scope;
beforeEach(inject(function($rootScope, $componentController){
  scope = $rootScope.$new();
  controller = $componentController('myComponent', {$scope: scope}, {myBinding: '1.5'});
}));

it('should expose my title', function() {
  expect(controller.myTitle).toBeDefined();
  expect(controller.myTitle).toBe('Unit Testing AngularJS');
});

it('should have my binding bound', function() {
  expect(controller.myBinding).toBeDefined();
  expect(controller.myBinding).toBe('1.5');
});

Patterns for unit testing

Not long ago I released a repository where I shared some of the common patterns that I’ve encountered while testing AngularJS applications. Now I’ve updated the example project with a component being tested as described in this post.

And although it’s early to start talking about patterns I guess the focus will be on testing the bindings and how the actions related to them affect the outside and the inside of our components. Even more now that we have one-way data binding.

Last week I joined the Bootcamp of my friends at Philos.io as a mentor and I created a small app where I started digging a bit on some of these patterns for my dumb components.

Advertisements

23 thoughts on “How to test Angular 1.5 components?

  1. Nice, but if you use “templateUrl” instead of “template”, Angular security blocks the execution of “$compile(…)”. Angular considers the path in “templateUrl” as a cross domain request.

  2. How do you test a component that uses a separately defined controller:

    .component('volunteers', {
      bindings: {},
      template: getTemplateStr('volunteers'),
      controller: "VolunteersComponentController as vcc"
    })
    

    I’ve tried to use $componentController but it errors out with: Unknown provider: VolunteersComponentControllerDirectiveProvider <- VolunteersComponentControllerDirective

    • Hi Rob!

      If you have registered the controller using the controller function as angular.module('webApp', []).controller('VolunteersComponentController', function() {}); then you can use the $controller method when testing the controller individually.

      var VolunteersComponentController;
      beforeEach(inject(function($controller) {
        VolunteersComponentController = $controller('VolunteersComponentController', {});
      }));
      

      Cheers, Pablo.

  3. Pablo,

    This technique looks really awesome! However, I am having trouble in my use case.

    My problem is that the component under test references another component in its HTML template:

    angular.module('my.project').component('a', new {
      template: 'This component A contains an inner component, B: <b></b>'
    }());
    
    angular.module('my.project').component('b', new {
     template: 'This is component B',
     controller: 'BController'
    }());
    

    When I am testing A, I’m getting a failure because I didn’t mock all of BController’s dependencies.

    What I’d like to do, ideally, is to create some dummy B component:
    {
    template: ‘Mock B’
    }
    …and test the interactions. How can I configure the AngularJS test framework to load this mock B component when compiling and linking the A component?

    I’m messing around with angular.mock.module, but I’m having trouble getting it to work.
    Any hint would be appreciated. Thanks!

    • Hi Rusell!

      You can have a look to this Stack Overflow question and all the answers.

      Basically you could replace your child component with a terminal mock directive using $compileProvider.

      beforeEach(module('my.project', function($compileProvider){
        $compileProvider.directive('b', function(){
          return {
            priority: 1000,
            terminal: true,
            restrict:'E',
            template:'<div>Mock B</div>'
          };
        });
      }));
      

      Cheers, Pablo.

  4. Thanks for the helpful article!
    I’m having a little trouble with testing component controllers that include a callback, as when the app is running angular wraps the callback, however this doesn’t happen when the test instantiates the controller. This leads me to having to ‘unwrap’ any parameters in my test, which feels a bit strange. For example, if I have a component with an updateItems callback
    bindings {
    updateItems: ‘&’
    }
    and some controller code
    function doSomething(){
    self.updateItems({
    items: itemsToPassToCallback
    });
    }
    then in my test the callback function will receive an object with the parameters, but in production there is some ‘magic’ angular code which will map the object to the function parameters. I was curious if you had any opinion on the best approach to take here.

    • Hi Graham!

      I’m not sure about what you mean with “angular wraps the callback”. Could you maybe post a JSFiddle, CodePen or Gist with more code?

      In any case, I would test the updateItems function in the parent component which is the one defining the functionality.

      Cheers, Pablo.

      • Thanks for the response, I’ve added a fiddle – https://jsfiddle.net/xwaov2eh/9/

        If you click the button then ‘text to pass to callback’ is logged in the console, however if you were to run the spec, you would see Object{text: ‘text to pass to callback’} logged, because the controller passes an object to the callback function.

        When angular binds the callback method to the controller it wraps it in another function, which is why you need to pass an object with param name -> param value pairs, however this makes the test look a bit funny.

        This isn’t a huge deal, I was just interested to get someone else’s opinion!

      • Hi Graham!

        Sorry for the late reply but I’m in the middle of my holidays and finally got some time.

        I’m not sure how are you testing this but I would create an spy and test that it has been called.

          var callback, controller, scope;
          beforeEach(inject(function($rootScope, $componentController){
            scope = $rootScope.$new();
            callback = jasmine.createSpy('callback');
            controller = $componentController('example', {$scope: scope}, {callback: callback});
          }));
        
          it('should pass the text to the callback', function() {
            var expected = {
              text: 'text to pass to callback'
            };
            controller.funcToTest();
            expect(callback).toHaveBeenCalled();
            expect(callback).toHaveBeenCalledWith(expected);
          });
        

        Cheers, Pablo.

  5. Hi Pablo,

    I’ve a parent app component that just has a ui-view directive, with no bindings or controller. When I create a mocked controller using $componentController, for some reason I get ‘Cannot export controller ” as ‘$ctrl’! No $scope object provided via `locals`.’ It works fine if I supply a scope from getting a new on rootscope. I have another test in different project that also has a similar component and the same test for it works fine! I don’t see any difference between both the tests though!! Are we supposed to pass a scope for app like root component that just has a ui-view directive and no bindings/controller?

    Sid

      • Ah right. It was me not knowing the limitations of `.find()` in the first plase. It says in angular docs: “find() – Limited to lookups by tag name”.
        I still tried it, but then switched to use the dom element (element[0]) and so lost the jqLite functions. It was the wrong approach in the first place.
        Thank you, your post and example project are very helpful!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s