Real unit testing in AngularJS
- 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).
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.
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.
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.
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
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.
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:
Inside the folder
In the end, you get something like this:
From within my e2e tests, I can now connect this page and interact with its elements. Here's how to use it:
Let's see what happens here. First, we connect the helper test, then we take
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.
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
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
Let us, we'll test it
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:
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
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
In tests, we again use Angular Mocks to inject into the modal window of the widget and service
We examined how to work with promise in both cases, in e2e and unit tests, using the following assert calls:
In the last example, we had a service that relied on a $ modal service that we used to make sure that it
Admission is as follows:
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
Here's how we could test something like that by locking dependencies
Sometimes you need to write a directive that has an isolated or child scope inside. For example, when using a service
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:
Madness, isn't it? First, we get wet again,
In conclusion, we get to the magic: we can call the
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?
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
describe
blocks 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
,should
andassert
. In the examples we will useexpect
. - 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.fulfilled
orexpect(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 WidgetsPage
function that can be connected in e2e tests. Five tests refer to it:widgetRepeater
: list of widgets contained inng-repeat
firstWidget
: first widget in the listwidgetCreateForm
: form for creating a widgetwidgetCreateNameField
: field for entering widget namewidgetCreateSubmit
: 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
expect
it WidgetsPage
from it. In beforeEach
we are loaded into the browser page. Then, in the example, we use the elements that we defined in WidgetsPage
to 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
isDisplayed
and 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
$modal
from 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
save
and cancel
methods 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
$modal
to 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 $modal
service. 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
done
as an argument to the example on our own, and done
when the test is completed.In tests, we again use Angular Mocks to inject into the modal window of the widget and service
$rootScope
from AngularJS. Having $rootScope
we 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 rejected
or 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
destroy
was 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 = this
in a blockbeforeEach
. - Create a copy and create methods, then make them the properties of an
self
object: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.withArgs
it 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
widgetStorage
service and updates widgets in its environment when the collection changes. There is also a method edit
that opens the one widgetEditor
we 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
widgetStorage
and 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
$dropdown
from 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 $dropdown
that 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,
widgetStorage
and widgetEditor
then we start writing the function compile
. This function will create two instances of scope, parentScope
and childScope
, we will fix the widget and put it in the child scope. Next, compile
the scope will be set up and a complex template: first, compile the parent element widget-organizer
into which the parent scope will be passed. When this is all done, we will add the child nested-widget
to it, passing the child scope and run it at the end $digest
. In conclusion, we get to the magic: we can call the
compile
function, 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 widgetEditor
was 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?