How to Unit Test AngularJS Events with Karma

Events are useful for communicating changes across components while keeping them decoupled. AngularJS provides a nice interface for this, including $broadcast and $on for publishing and subscribing. Luckily, these aspects of an application can be easily tested using Karma.

These examples assume that you already have the infrastructure for an AngularJS application in place and have Karma configured for testing.

Verifying $broadcast Events

Say there is a controller that broadcasts an event when a button is clicked. The event declares that an action has occurred, and it includes some data from the context of that action. A typical chunk of controller code to do such a thing would look something like this:

ctrl.onButtonClick = function () {
    $rootScope.$broadcast('event.action', {
        attributeOne: 'value 1' 
    });
};

And let’s say the button code looks like this:

<button class="app-button" ng-click="ctrl.onButtonClick()">

To test this, we can write a simple Karma test:

var compiledDirective;

beforeEach(ngMock.inject(function ($rootScope, $compile) {
    scope = $rootScope.$new();

    var content = '<my-directive></my-directive>';

    compiledDirective = $compile(content)(scope);
    scope.$digest();
}));

it('broadcasts an event when the button is clicked', function () {
    var data = null;

    scope.$on('event.action', function ($event, eventData) {
        data = eventData;
    });

    compiledDirective.find('.app-button').click();

    expect(data.attributeOne).to.equal('value 1');
});

There are a few things going on here:

  1. myDirective is compiled and instantiated. This creates compiledDirective, which is a live instance of the directive. By “live,” I mean that it is running in the browser just as it would if it were a part of the application. This allows tests to interact with directives through the browser and trigger reactions naturally with minimal mocking.
  2. ‘$on’ is used to register a subscription for the event before the test interacts with the component. Since data is initialized to null, if the event does not fire, data will not be set.
  3. find('.app-button') is using a CSS class selector to target the button element. This returns a jqLite object, which has a similar API as a jQuery object. Once the element is brought into scope, the test can interact with it. For example, click().
  4. Finally, an assertion framework like Chai can be used to confirm that the event fired and populated data.attributeOne.

Verifying $on Subscriptions

Usually, broadcasting an event is only half of the equation. In order to test the consumer side of the event cycle, a similar process can be followed. Let’s assume that there is a subscriber that is going to set a value to show a dialog when the event occurs.

$rootScope.$on('event.action', function () {
    ctrl.hasEventHappened = true;
});

The dialog template is simple and toggles a class based on the value of ctrl.hasEventHappened.

<div class="app-dialog" ng-class="{ 'is-open' : ctrl.hasEventHappened }"></div>

This functionality can be tested by a spec that broadcasts the event and asserts that the dialog is open.

it('shows the dialog when an event action occurs', function () {
    var dialog = compiledDirective.find('.app-dialog');

    expect(dialog.hasClass('is-open'), 'expected dialog to be hidden').to.equal(false);

    $rootScope.$broadcast('event.action');

    expect(dialog.hasClass('is-open'), 'expected dialog to be visible').to.equal(true);
});

This is what’s happening here:

  1. The dialog is selected using find and a CSS class selector.
  2. A check is done to confirm that the dialog is hidden prior to the event.
  3. $broadcast is used to create a fake event and trigger the dialog.
  4. Finally, the test asserts that the dialog has the is-open class after the event cycle completes.

These tests turn out to be pretty solid. A spec is able to interact with a directive element and make some assertions with only a couple lines of code.