Real unit testing in AngularJS

Original author: Ben Lewis
  • Transfer
AngularJS is young and hot when it comes to modern web development. Its unique approach to compiling HTML and two-way data binding makes it an effective tool for building client-side web applications. When I found out that Quick Left (the studio where the author works. Approx. Per.) Will use it to create an application for one of our clients, I was excited and tried to find out as much about angular as I could. I went around the Internet, every lesson and guide I could find on Google. They were really helpful in understanding how directives, templates, compilation, and the event loop (digest) work, but when it came to testing, I found that this topic was simply overlooked.

I studied the TDD (Development through Testing) approach and I feel at ease without the Red-Green-Refactoring approach. Since we were still figuring out what was happening in Angular testing, the team sometimes had to rely on the testing-after approach. It started to make me nervous, so I decided to focus on testing. I spent weeks on it, and soon the test coverage rose from 40% to 86% (By the way, if you haven’t done this yet, you can try Istabul to check the code coverage in your JS application).


Introduction


Today I want to share some of the things I learned. As good as the Angular documentation , testing a combat application is rarely as simple as the examples you see below. There are many pitfalls that I had to go through to get some things to work. I found several workarounds that came in handy again and again. In this article we will look at some of them.

  • Reusing Pages in End-to-End (e2e) Tests
  • Work with functions returning Promise
  • Mocking controller dependencies and directives
  • Access to child and isolated scope


This article is intended for intermediate to advanced developers using AngularJS to write combat applications that can help reduce the pain of testing. I hope a sense of security in the testing workflow allows the reader to start practicing the TDD approach and developing more robust applications.

Testing tools


There are many frameworks and testing tools available to Angular for the developer, and perhaps you already have your preferences. Here is a list of tools that we have chosen and will use throughout the article.

  • Karma : Test Launcher from the AngularJS Team. Use it to launch Chrome, Firefox, and PhantomJS.
  • AngularMocks : Gives support for injection and Mock Angular services in unit testing.
  • Protractor : A functional testing tool for AngularJS that runs your application in a browser and interacts with it through Selenium.
  • Mocha : A framework written for node.js for testing. It makes it possible to write describeblocks and do checks in them.
  • Chai : Assertion library which integrates into Mocha, and gives access to the BDD approach and the ability to write statements expect, shouldand assert. In the examples we will use expect.
  • Chai-as-promised : A plugin for Chai that is really useful when working with functions that return promise. It gives us the opportunity to write like this:, expect(foo).to.be.fulfilledor expect(foo).to.eventually.equal(bar).
  • Sinon : Stub and Mock library. Use it to create dependency stubs in your directives and controllers, and verify that there was a function call with the correct arguments.
  • Browserify : Allows you to easily connect modules between files in a project.
  • Partialify : Allows you to include HTML templates directly in AngularJS directives.
  • Lodash : A library with buns and sugar that extends the standard JavaScript functionality.


Setting Up Helpers for the Test


Let's start by writing a helper that will connect the dependencies we need. Here we will use Angular Mocks, Chai, Chai-as-promised and Sinon

// test/test-helper.js// подключаем наш проектrequire('widgetProject');
// зависимостиrequire('angular-mocks');
var chai = require('chai');
chai.use('sinon-chai');
chai.use('chai-as-promised');
var sinon = require('sinon');
beforeEach(function() {
  // создаем новую песочницу перед каждым тестомthis.sinon = sinon.sandbox.create();
});
afterEach(function() {
  // чистим песочницу, чтобы удалить все стабыthis.sinon.restore();
});
module.exports = {
  rootUrl: 'http://localhost:9000',
  expect: chai.expect
}

Getting Started: Top-Down Testing


I am a big proponent of the top-down testing style. It all starts with the functionality that I want to create, I write a pseudo script describing the functionality and create a feature test. I run this test and it fails with an error. Now I can start designing all the parts of the system that I need for the feature test to work, using unit tests that guide me along the way.

For example, I will create an imaginary application “Widgets”, which can display a list of widgets, create new ones, and edit current ones. The code that you see here is not enough to build a full-fledged application, but enough to understand sample tests. We'll start by writing an e2e test that describes the behavior of creating a new widget.

Reuse Pages in e2e Testing


When working on a one-page application, it makes sense to observe the DRY principle by writing reusable “pages” that can be connected to many e2e tests.

There are many ways to structure tests in an Angular project. Today, we will use the following structure:

widgets-project
|-test
|  |
|  |-e2e
|  |  |-pages
|  |
|  |-unit

Inside the folder pages, we will create a WidgetsPagefunction that can be connected in e2e tests. Five tests refer to it:

  • widgetRepeater: list of widgets contained in ng-repeat
  • firstWidget: first widget in the list
  • widgetCreateForm: form for creating a widget
  • widgetCreateNameField: field for entering widget name
  • widgetCreateSubmit: submit form button

In the end, you get something like this:

// test/e2e/pages/widgets-page.jsvar helpers = require('../../test-helper');
functionWidgetsPage() {
  this.get = function() {
    browser.get(helpers.rootUrl + '/widgets');
  }
  this.widgetRepeater = by.repeater('widget in widgets');
  this.firstWidget = element(this.widgetRepeater.row(0));
  this.widgetCreateForm = element(by.css('.widget-create-form'));
  this.widgetCreateNameField = this.widgetCreateForm.element(by.model('widget.name');
  this.widgetCreateSubmit = this.widgetCreateForm.element(by.buttonText('Create');
}
module.exports = WidgetsPage

From within my e2e tests, I can now connect this page and interact with its elements. Here's how to use it:
// e2e/widgets_test.jsvar helpers = require('../test-helper');
var expect = helpers.expect;
var WidgetsPage = require('./pages/widgets-page');
describe('creating widgets', function() {
  beforeEach(function() {
    this.page = new WidgetsPage();
    this.page.get();
  });
  it('should create a new widget', function() {
    expect(this.page.firstWidget).to.be.undefined;
    expect(this.page.widgetCreateForm.isDisplayed()).to.eventually.be.true;
    this.page.widgetCreateNameField.sendKeys('New Widget');
    this.page.widgetCreateSubmit.click();
    expect(this.page.firstWidget.getText()).to.eventually.equal('Name: New Widget');
  });
});

Let's see what happens here. First, we connect the helper test, then we take expectit WidgetsPagefrom it. In beforeEachwe are loaded into the browser page. Then, in the example, we use the elements that we defined in WidgetsPageto interact with the page. We check that there are no widgets, fill out the form to create one of them with the value “New Widget” and check that it is displayed on the page.

Now, dividing the logic for the form into a reusable “page”, we can use it repeatedly to test form validation, for example, or later in other directives.

Work with functions returning Promise


Assert methods, which we took from Protractor'a in the test above, the Promise return, so we use the Chai-as-promised to test that function isDisplayedand getText return what we expect.

We can also work with promise objects inside unit tests. Let's look at an example in which we are testing a modal window that can be used to edit an existing widget. It uses a service $modalfrom UI Bootstrap. When the user opens a modal window, the service returns a promise. When it cancels or saves the window, promise is resolved or rejected.
Let us, we'll test it saveand cancelmethods are properly connected, utilizing Chai-as-promised.

// widget-editor-service.jsvar angular = require('angular');
var _ = require('lodash');
angular.module('widgetProject.widgetEditor').service('widgetEditor', ['$modal', '$q', '$templateCache', function (
  $modal,
  $q,
  $templateCache
) {
  returnfunction(widgetObject) {
    var deferred = $q.defer();
    var templateId = _.uniqueId('widgetEditorTemplate');
    $templateCache.put(templateId, require('./widget-editor-template.html'));
    var dialog = $modal({
      template: templateId
    });
    dialog.$scope.widget = widgetObject;
    dialog.$scope.save = function() {
      // Здесь сохраняем что-нибудь
      deferred.resolve();
      dialog.destroy();
    });
    dialog.$scope.cancel = function() {
      deferred.reject();
      dialog.destroy();
    });
    return deferred.promise;
  };
}]);

The service will load the widget editing template into the template cache, the widget itself, and create a deferred object that will be allowed or rejected, depending on whether the user rejects or saves the editing form, which returns a promise.

Here's how to test something like this:

// test/unit/widget-editor-directive_test.jsvar angular = require('angular');
var helpers = require('../test_helper');
var expect = helpers.expect;
describe('widget storage service', function() {
  beforeEach(function() {
    var self = this;
    self.modal = function() {
      return {
        $scope: {},
        destroy: self.sinon.stub()
      }
    }
    angular.mock.module('widgetProject.widgetEditor', { $modal: self.modal });
  });
  it('should persist changes when the user saves', function(done) {
    var self = this;
    angular.mock.inject(['widgetModal', '$rootScope', function(widgetModal, $rootScope) {
      var widget = { name: 'Widget' };
      var promise = widgetModal(widget);
      self.modal.$scope.save();
      // каким то образом протестировали сохранение виджета
      expect(self.modal.destroy).to.have.been.called;
      expect(promise).to.be.fulfilled.and.notify(done);
st
      $rootScope.$digest();
    }]);
  });
  it('should not save when the user cancels', function(done) {
    var self = this;
    angular.mock.inject(['widgetModal', '$rootScope', function(widgetModal, $rootScope) {
      var widget = { name: 'Widget' };
      var promise = widgetModal(widget);
      self.modal.$scope.cancel();
      expect(self.modal.destroy).to.have.been.called;
      expect(promise).to.be.rejected.and.notify(done);
      $rootScope.$digest();
    }]);
  });
});

To cope with the complexity of the promise, which returns a modal window in the widget edit test, we can do several things. Create a mock from a service $modalto a function beforeEach, replacing the output of the function with an empty object $scope, and stop the call destroy. In angular.mock.module, we pass a copy of the modal window so that Angular Mocks can use it instead of the real $modalservice. This approach is quite useful for stub dependencies, which we will soon see.

We have two examples, and everyone should wait for the promise result returned by the editing widget before it completes. In this regard, we must pass doneas an argument to the example on our own, and donewhen the test is completed.

In tests, we again use Angular Mocks to inject into the modal window of the widget and service $rootScopefrom AngularJS. Having $rootScopewe can cause a cycle $digest. In each of the tests, we load the modal window, cancel or enable it, and use Chai-as-expected to check whether the promise is returned as rejectedor how resolved. For the actual call to promise and destroy, we have to start $digest , so it is called at the end of each assert block.

We examined how to work with promise in both cases, in e2e and unit tests, using the following assert calls:

  • expect(foo).to.eventually.equal(bar)
  • expect(foo).to.be.fulfilled
  • expect(foo).to.be.rejected


Directives and Controllers Mock Dependencies


In the last example, we had a service that relied on a $ modal service that we used to make sure that it destroywas really called. The technique we used is quite useful and allows unit tests to work more correctly in Angular.

Admission is as follows:

  • Assign var self = thisin a block beforeEach.
  • Create a copy and create methods, then make them the properties of an selfobject:

    self.dependency = {
      dependencyMethod: self.sinon.stub()
    }
    
  • Transfer copies to the module under test:
    angular.mock.module('mymodule', {
      dependency: self.dependecy,
      otherDependency: self.otherDependency
    });
    
  • Check locking methods in test cases. You can use expect(foo).to.have.been.called.withArgsit by passing the arguments you expect for better coverage.

Sometimes directives or controllers depend on many internal and external dependencies, and you need to lock them all.
Let's take a look at a more complex example, in which the directive monitors the widgetStorageservice and updates widgets in its environment when the collection changes. There is also a method editthat opens the one widgetEditorwe created earlier.
// widget-viewer-directive.jsvar angular = require('angular');
angular.module('widgetProject.widgetViewer').directive('widgetViewer', ['widgetStorage', 'widgetEditor', function(
  widgetStorage,
  widgetEditor
) {
  return {
    restrict: 'E',
    template: require('./widget-viewer-template.html'),
    link: function($scope, $element, $attributes) {
      $scope.$watch(function() {
        return widgetStorage.notify;
      }, function(widgets) {
        $scope.widgets = widgets;
      });
      $scope.edit = function(widget) {
        widgetEditor(widget);
      });
    }
  };
}]);

Here's how we could test something like that by locking dependencies widgetStorageand widgetEditor:

// test/unit/widget-viewer-directive_test.jsvar angular = require('angular');
var helpers = require('../test_helper');
var expect = helpers.expect;
describe('widget viewer directive', function() {
  beforeEach(function() {
    var self = this;
    self.widgetStorage = {
      notify: self.sinon.stub()
    };
    self.widgetEditor = self.sinon.stub();
    angular.mock.module('widgetProject.widgetViewer', {
      widgetStorage: self.widgetStorage,
      widgetEditor: self.widgetEditor
    });
  });
  // Остальная часть теста...
});

Access to Subsidiary and Isolated Scope


Sometimes you need to write a directive that has an isolated or child scope inside. For example, when using a service $dropdownfrom Angular Strap , an isolated scope is created. Accessing this scope can be quite a painful task. But knowing about self.element.isolateScope()you can fix it. Here is one use case $dropdownthat creates an isolated scope:

// nested-widget-directive.jsvar angular = require('angular');
angular.module('widgetSidebar.nestedWidget').directive('nestedSidebar', ['$dropdown', 'widgetStorage', 'widgetEditor', function(
  $dropdown,
  widgetStorage,
  widgetEditor
) {
  return {
    restrict: 'E',
    template: require('./widget-sidebar-template.html'),
    scope: {
      widget: '='
    },
    link: function($scope, $element, $attributes) {
      $scope.actions = [{
        text: 'Edit',
        click: 'edit()'
      }, {
        text: 'Delete',
        click: 'delete()'
      }]
      $scope.edit = function() {
        widgetEditor($scope.widget);
      });
      $scope.delete = function() {
        widgetStorage.destroy($scope.widget);
      });
    }
  };
}]);

Assuming that the directive inherits the widget from the parent directive, which has a collection of widgets, accessing the child scope can be quite difficult to check if its properties have changed as expected. But it can be done. Let's take a look at:

// test/unit/nested-widget-directive_test.jsvar angular = require('angular');
var helpers = require('../test_helper');
var expect = helpers.expect;
describe('nested widget directive', function() {
  beforeEach(function() {
    var self = this;
    self.widgetStorage = {
      destroy: self.sinon.stub()
    };
    self.widgetEditor = self.sinon.stub();
    angular.mock.module('widgetProject.widgetViewer', {
      widgetStorage: self.widgetStorage,
      widgetEditor: self.widgetEditor
    });
    angular.mock.inject(['$rootScope', '$compile', '$controller', function($rootScope, $compile, $controller) {
      self.parentScope = $rootScope.new();
      self.childScope = $rootScope.new();
      self.compile = function() {
        self.childScope.widget = { id: 1, name: 'widget1' };
        self.parentElement = $compile('<widget-organizer></widget-organizer>')(self.parentScope);
        self.parentScope.$digest();
        self.childElement = angular.element('<nested-widget widget="widget"></nested-widget>');
        self.parentElement.append(self.childElement);
        self.element = $compile(self.childElement)(self.childScope);
        self.childScope.$digest();
      }]);
    });
    self.compile();
    self.isolateScope = self.element.isolateScope();
  });
  it('edits the widget', function() {
    var self = this;
    self.isolateScope.edit();
    self.rootScope.$digest();
    expect(self.widgetEditor).to.have.been.calledWith(self.childScope.widget);
  });


Madness, isn't it? First, we get wet again, widgetStorageand widgetEditorthen we start writing the function compile. This function will create two instances of scope, parentScopeand childScope, we will fix the widget and put it in the child scope. Next, compilethe scope will be set up and a complex template: first, compile the parent element widget-organizerinto which the parent scope will be passed. When this is all done, we will add the child nested-widgetto it, passing the child scope and run it at the end $digest.

In conclusion, we get to the magic: we can call the compilefunction, then crawl into the compiled isolated scope of the template (which is the scope from $dropdown) through self.element.isolateScope(). At the end of the test, we can get into an isolated scope to calledit, and finally verify that the docked one widgetEditorwas called up with the docked widget.

Conclusion


Testing can be painful. I remember several cases when in our project there was so much pain in figuring out how to do it all, that it was a temptation to return to writing code and “click testing” to test it’s working. Unfortunately, when you exit this process, the feeling of uncertainty only increases.

After we took the time to understand how to deal with complex cases, it became much easier to understand when such cases again occur. Armed with the techniques described in this article, we were able to join the TDD process and confidently moved forward.

I hope that the techniques that we saw today will be useful in your daily practice. AngularJS is still a young and growing framework. What techniques do you use?

Also popular now: