Unit testing Angular 1.5 components – a detailed guide [“NG 1.5 from the trenches” 6/7]

This post is the 6th part of the “Angular 1.5 from the trenches” series, presenting the architecture of a component-oriented, “NG 2 ready” Angular 1.5 app we’re building.
The module.component() method, introduced in Angular 1.5, may seem only a cosmetic addition, but the approach it promotes (and which we utilise to the full extent) results in a very different flavour of architecture than most of the “classic” Angular tutorials describe, so I hope you’ll learn a thing or two from our experience.

Table of Contents:

Components are the most tricky part of Angular 1.5 when it comes to testing. They are bound to HTML templates and DOM events, they have “automagically” instantiated attribute bindings, and they are composed into a tightly coupled component tree, without an obvious way to isolate parent and child components.

There are many possible approaches to testing components, but the one we’ve developed in our project works well for us and has proven itself during over 6 months with a big app, so I’d like to share it.

Black-box vs white-box component testing

You can approach testing Angular 1.5 components in one of the two ways: black-box or white-box.

black-box testing

In this approach, you treat a fully configured component (with a template, attribute bindings etc.) as a closed whole. The interface of such a component becomes its template – you test it through the rendered HTML by simulating clicks, filling input fields, and verifying the resulting HTML structure.

white-box testing

In this approach, you directly access the component’s controller (through the $componentController service provided by Angular). You then simulate user actions and verify the component state by directly accessing its methods and fields.

why we prefer black-box testing

We’re very strict about keeping our templates free from any logic. The only thing we allow in our templates are direct bindings to the component controller’s fields and event handling methods. This is still error-prone, though. You can make a typo in a field name, pass wrong parameters to the event handler, incorrectly set attributes of a child component, etc. This is a frequent source of annoying little bugs.

Also, a lot of our components are simple “View Components”, with a minimal amount of logic in a controller (or even no logic and no controller at all). There is nothing to test for such components in a white-box approach – but still, we want to know if these component work.

You can argue that verifying template output and wiring of child components is the responsibility of integration tests and that the unit tests should be concerned only about the component logic. This is a perfectly valid point, but it depends on what do you consider a “unit” – is it a single class (component’s controller) or is the whole component, with all its configuration?

No matter how do you call it, though, it’s hard to argue that this is a vital part of verifying if the whole app works correctly and should be tested somehow. We try to not be dogmatic but rather practical about it, and we’ve found that treating the whole components as units works better for us.

The “mockist” approach (testing components in full isolation)

We use the so-called mockist unit testing approach. In the case of Angular 1.5 component tests, this means that we stub all the services the component depends on, and also all its child components.

The mockist approach is especially well suited for our black-box approach to testing components. Because we test the component as a whole, together with the template, if we didn’t stub the child components we would render not only the component under test but the whole component tree every time. This would be expensive and hard to set-up in tests.

While stubbing services is straightforward and Angular supports it well with its Dependency Injection system, stubbing child components is more tricky. We do it by shallow rendering child components. Angular testing helpers don’t provide shallow rendering (like e.g. React), so we had to invent our custom solution – I’ll present it in the later part of the post.

Common component testing scenarios

In the following sections, I’ll focus on several common scenarios you’ll surely encounter when testing components, especially the ones involving parent-child communication. I’ll show how to use the mockist approach and shallow rendering in these popular scenarios.

Testing other Angular 1.5 elements (services, filters etc.) is more straightforward and well described in many other blog posts and tutorials, so I’ll focus solely on NG 1.5 components in this post.

testing receiving data from parent through attribute bindings

Imagine a simple component like below:

angular.module('some.module').component('someComponent', {
  bindings: {
    someAttr: '<'
  },
  template: `
    <div>
    	<span class="js-some-attr">{{ $ctrl.someAttr }}</span>
    </div>
  `
});

It receives data from the parent component through a single attribute binding (“) and renders this data in a template.

To test this component we need to simulate rendering it in its parent template and then validate the rendered HTML.

That’s how we do it:

describe('Some Component', () => {

  let parentScope;
  let element;  

  beforeEach(angular.mock.module('some.module'));

  // 1:
	beforeEach(inject(($compile, $rootScope) => {

    // 2:
		parentScope = $rootScope.$new();
    parentScope.someAttr = 'SOME_VALUE';

    // 3:
		element = angular.element(`<some-component some-attr="someAttr"></some-component>`);
		$compile(element)(parentScope);

    // 4:
    parentScope.$digest();

	}));

	it('displays initial value of some attr', () => {

    // 5:
    const someAttrValue = findIn(element, '.js-some-attr').text();
		expect(someAttrValue).toEqual('SOME_VALUE');

	});

  it('displays changed value of some attr', () => {

    // 6:
    parentScope.someAttr = 'CHANGED_VALUE';
    parentScope.$digest();

    const someAttrValue = findIn(element, '.js-some-attr').text();
    expect(someAttrValue).toEqual('CHANGED_VALUE');

  });
});

The most interesting stuff that happens in the test:

  1. We inject $compile and $rootScope Angular services. We need them to render our tested component and to simulate its parent component.
  2. We create the scope that will simulate the scope of the parent component and set some fields on this scope. These fields simulate the fields normally set on a component’s controller.
  3. We compile our tested component with the parentScope. Because of this, the fields we’ve set on the parentScope can be used as attributes passed to our tested component.
  4. We initiate the Angular’s digest loop to make our changes visible.
  5. Now we can verify the rendered component’s template. Our preferred method is to add explicit CSS classes to a few selected tags containing values we want to test (with the js- prefix, to make it clear which CSS classes are used for styling elements and which ones to address values in tests). We extract the value we want to verify using Angular’s built-in jqLite and the DOM Node API and run some expectations on it. We encapsulate the tag finding logic into a reusable helper, but it is really simple:
      function findIn(element, selector) {
        return angular.element(element[0].querySelector(selector));
      }
      
  6. In a similar fashion as we set up the parentScope initially, in the beforeEach block, we can set it again in any given test, thus simulating what happens when the value of a parent component’s controller field changes.

testing invoking parent controller callbacks through an ‘&’ binding

Again, imagine a simple component:

angular.module('some.module').component('someComponent', {
  bindings: {
    onSayHello: '&'
  },
  template: `
    <div>
      <button class="js-say-hello" ng-click="$ctrl.onSayHello({ message: 'HELLO' })">say hello</span>
    </div>
  `
});

It contains a single button and, when this button is clicked, calls the parent method passed down through an ‘&’ binding, with a single parameter message.

To test this component we have to simulate user clicking a button, and then verify if the proper parent controller callback was called with a correct parameter:

describe('Some Component', () => {

  let parentScope;
  let element;  

  beforeEach(angular.mock.module('some.module'));

  beforeEach(inject(($compile, $rootScope) => {

    // 1:
    parentScope = $rootScope.$new();
    parentScope.sayHelloSpy = jasmine.createSpy('sayHelloSpy');

    // 2:
    element = angular.element(`<some-component on-say-hello="sayHelloSpy(message)"></some-component>`);

    $compile(element)(parentScope);
    parentScope.$digest();

  }));

  it('invokes action with a param', () => {        

    // 3:
    const sayHelloButton = findIn(element, '.js-say-hello');
    sayHelloButton.triggerHandler('click');

    // 4:
    expect(parentScope.sayHelloSpy).toHaveBeenCalledWith('HELLO');

  });
});

The most interesting stuff in the above test:

  1. We set up the parentScope in a similar way like in the test of passing an attribute, but this time instead of a hardcoded value we assign a Jasmine spy to the parentScope field, to simulate a callback method.
  2. We pass the parentScope field to the tested component through the attribute binding, but this time as a method call not a value.
  3. We find a button using the same method as in the case of finding a tag containing a value, but this time instead of extracting a value we simulate a click, using Angular’s triggerHandler test helper.
  4. We check if the spy was called with the correct parameter.

testing passing data down to a child component

Imagine a component that receives some data and passes part of this data down to a child component:

angular.module('some.module').component('someComponent', {
  bindings: {
    parentAttr: '<'
  },
  controller: class {
    get childAttr() {
      return parentAttr.someField;
    }
  },
  template: `
    <div>
      <child-component child-attr="$ctrl.childAttr"></child-component>
    </div>
  `
});

To test it, we have to assert somehow what data got passed to a child component. One option to do this is to render the whole component tree and extract this value from the resulting HTML. This has several drawbacks, as the whole component tree may be potentially very complex (a child component can have its own children and so on) and it breaks encapsulation, as in the parent component test we have to know the structure of a child component template. This is OK in the end-to-end tests, but in the unit tests, we would like to avoid it. What we want is to use shallow rendering (avoid rendering the whole template of the child component) and somehow assert what attributes were passed to the child component without having to parse and know its template.

Angular 1.5 doesn’t offer shallow rendering out-of-the-box, but it can be achieved with a simple helper:

export function componentSpyOn(name) {
  function componentSpy($provide) {
    componentSpy.bindings = [];

    $provide.decorator(name + 'Directive', ($delegate) => {
      let component = $delegate[0];

      component.template = '';
      component.controller = class {
        constructor() {
          componentSpy.bindings.push(this);
        }
      };

      return $delegate;
    });
  }

  return componentSpy;
}

Having the above helper at our disposal, we can write the following test:

describe('Some Component', () => {

  // 1:
  const childComponentSpy = componentSpyOn('childComponent');

  let parentScope;
  let element;

  // 2:
  beforeEach(angular.mock.module('some.module', childComponentSpy));

  beforeEach(inject(($compile, $rootScope) => {

    // 3:
    parentScope = $rootScope.$new();
    parentScope.parentAttr = { someField: 'SOME_FIELD_VALUE' };

    // 4:
    element = angular.element(`<some-component parent-attr="parentAttr"></some-component>`);
    $compile(element)(parentScope);

    parentScope.$digest();

  }));

  it('passes parentAttr.someField to a child', () => {    

    // 5:
    const childAttrValue = childComponentSpy.bindings[0].childAttr;
    expect(childAttrValue).toEqual('SOME_FIELD_VALUE');

  });
});

The most interesting stuff happening in the test:

  1. We replace a child component with a spy (using our shallow rendering helper described above) and assign this spy to a variable to be able to use it later.
  2. We pass the spy to the Angular mock.module method. This results in the child component being stubbed (its declaration in Angular’s Dependency Injector is overwritten by our spy).
  3. We simulate passing some complex object to the parent component. We will later verify if the parent correctly passes one of this object fields into the child component.
  4. We render the parent component as usual.
  5. We use our child component spy to verify if the parent correctly sets the child-attr attribute on a child component in the template.

testing binding a callback to a child component

A parent component not only passes data to its children but also gets notified by them about user actions. Imagine such a component, that binds to the child component’s action:

angular.module('some.module').component('someComponent', {  
  controller: class {
    onChildAction(someParam) {
      this.sentFromChild = someParam;
    }
  },
  template: `
    <div>
      <child-component on-child-action="$ctrl.onChildAction(someParam)"></child-component>
      <div class="js-sent-from-child">{{ $ctrl.sentFromChild }}</div>
    </div>
  `
});

To test this component we have to somehow simulate that the child component invoked an action and then verify if the parent component handled this action correctly. Again, we’re using our shallow rendering component spy helper for this:

describe('Some Component', () => {

  // 1:
  const childComponentSpy = componentSpyOn('childComponent');

  let parentScope;
  let element;

  // 2:
  beforeEach(angular.mock.module('some.module', childComponentSpy));

  // 3:
  beforeEach(inject(($compile, $rootScope) => {
    parentScope = $rootScope.$new();
    element = angular.element(`<some-component></some-component>`);
    $compile(element)(parentScope);
  }));

  it('handles an action from a child', () => {    

    // 4:
    const childAction = childComponentSpy.bindings[0].onChildAction;
    childAction({ someParam: 'SOME_VALUE' });
    parentScope.$digest();

    // 5:
    const sentFromChild = findIn(element, 'js-sent-from-child').text();
    expect(sentFromChild).toEqual('SOME_VALUE');

  });
});

The most interesting parts of the test:

  1. We spy on the child component similarly as before.
  2. And we pass our spy to the mock.module method to override the child component – again the same as before.
  3. We render our parent component as usual.
  4. We simulate an action being invoked in the child component by accessing a binding through our component spy, manually invoking this action, and running Angular’s digest loop to kick off parent component’s action handling logic.
  5. We check if the parent component reacted correctly to the child component’s action (again by parsing resulting HTML with the help of our findIn function).

testing sending events

As I’ve described when discussing Angular 1.5 cross-component communication, some of the components communicate through events. Take a look at such a simple, event sending component:

angular.module('some.module').component('someComponent', {
  controller: class {
    constructor($scope) {
      this.$scope = $scope;
    }

    emitEvent() {
      this.$scope.$emit('some.action', 'HELLO');
    }
  },
  template: `
    <div>
      <button class="js-emit-event" ng-click="$ctrl.emitEvent()">emit someAttr value up the component tree</button>
    </div>
  `
});

To test it, we need to simulate a component higher in the component tree hierarchy (a parent or a grandparent of our tested component) that will subscribe to the event sent by our tested component. Then we must check if the event was sent with the correct parameters:

describe('Some Component', () => {

  let parentScope;
  let element;

  beforeEach(angular.mock.module('some.module'));

  // 1:
  beforeEach(inject(($compile, $rootScope) => {
    parentScope = $rootScope.$new();
    element = angular.element(`<some-component></some-component>`);
    $compile(element)(parentScope);
  }));

  it('emits an event up the component tree', () => {

    // 2:
    const someActionSpy = jasmine.createSpy('someActionSpy');
    parentScope.$on('some.action', someActionSpy);
    parentScope.$digest();

    // 3:
    const emitEventButton = findIn(element, '.js-emit-event');
    emitEventButton.triggerHandler('click');

    // 4:
    expect(someActionSpy).toHaveBeenCalledWith(jasmine.anything(), 'HELLO');

  });
});

The most interesting parts of the above test:

  1. We render our tested component as usual.
  2. To simulate subscribing to an event, we create a jasmine spy and bind this spy as the event handler on the parentScope (using standard Angular’s $on method).
  3. Next, we emit the event from our tested component, by finding a template element responsible for sending the event and simulating clicking it.
  4. Finally, we check if our fake event handler was called with correct parameters (the first parameter to Angular’s event handler is always an event object, that we’re usually not interested in – so we set an expectation for it as jasmine.anything()).

testing handling events

When components communicate through events, both sides: the sender and the receiver need to be tested. In the previous section I’ve shown how to test event sender, now let’s see how to test a simple event receiver like below:

angular.module('some.module').component('someComponent', {
  controller: class {
    constructor($scope) {
      $scope.$on('some.action', (event, someParam) => {
        this.actionParam = someParam;
      });
    }
  },
  template: `
    <div>
      <child-component></child-component>
      <span class="js-action-param">{{ $ctrl.actionParam }}</span>      
    </div>
  `
});

To test a component like this, we have to simulate an event being sent from one of its child components and verify if the tested component handled the event properly:

describe('Some Component', () => {

  const childComponentSpy = componentSpyOn('childComponent');

  let parentScope;
  let element;

  beforeEach(angular.mock.module('some.module', childComponentSpy));

  // 1:
  beforeEach(inject(($compile, $rootScope) => {
    parentScope = $rootScope.$new();
    element = angular.element(`<some-component></some-component>`);
    $compile(element)(parentScope);
  }));

  it('receives someParam emitted from some grand-child', () => {

    // 2:
    parentScope.$broadcast('some.action', 'SOME_PARAM_VALUE');
    parentScope.$digest();

    // 3:
    const actionParamValue = findIn(element, '.js-action-param').text();
    expect(actionParamValue).toEqual('SOME_PARAM_VALUE');

  });
});

The most important parts of the test:

  1. We render our tested component as usual.
  2. We simulate sending an event. Here, we use a trick. Normally the tested component would listen to the event sent from the inside, from one of its child components (which will use the $emit method). However, simulating sending an event from the inside is really hard and would require very complicated mocking. But Angular has two ways of sending events: from the inside (bubbling up) through the $emit method and from the outside (bubbling down) through the $broadcast method. Architecturally these two approaches are different, but from the point of view of the event receiver (using the $on method), there’s no difference if the event hit the component from the inside or outside. We can utilize this and simulate sending the event from the parentScope via $broadcast instead of from the child component via $emit – our tested component won’t know the difference and the test will be much simpler.
  3. Finally, we check if our tested component correctly handled the event.

Conclusion

Testing Angular 1.5 components can be tricky, but our black-box, mockist approach works pretty well for us. It let us write and maintain a robust, thorough suite of tests for our component oriented NG 1.5 app.

Over to you

Of course, our preferred way is not the only way of testing NG 1.5 components. I’d love to hear how do you test your Angular components and why have you chosen such approach!

Subscribe for the rest of the “NG 1.5 from the trenches” series

If you want to get notified when the next posts in the series get published, subscribe to my blog via RSS, follow me on Twitter or subscribe below via mail:

Enter your email address to follow this blog and receive notifications of new posts by email.

3 thoughts on “Unit testing Angular 1.5 components – a detailed guide [“NG 1.5 from the trenches” 6/7]

  1. Interesting, but what if the template of the component is external (loaded with templateUrl:”) ? Then the test will fail with unexpected request error (trying to load the template). How can we go around this issue?

    Liked by 1 person

What do you think?

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