Three sanity-preserving ideas that will make me and you 10x more productive with real-world AngularJS applications
This is the second in a three-part series on practical large-scale development with AngularJS. The TL;DR version is at the end of the article. If you have not read the first part, you might want to start with it. It lays out the foundation for the structure of a large-scale AngularJS project. In this part, I focus on automatic testing with AngularJS and ng-boilerplate project template.
Part 2: Enjoyable Automated Testing
Automatic testing is easy, natural, and helpful. Huh, those words felt odd to write. Like most developers, I generally agree that yes, yes, of course unit tests are important, but in reality, under the pressures of schedule and the changing requirements, the unit test suite gets pushed to the side and eventually forgotten. Temporarily, of course, just until we get through this urgent set of bugfixes and get that next chunk of budget approved. But then we temporarily push the unit tests out again, and we only remember about it when a major refactoring leaves a trail of bugs that keep popping up months after the release. Then we swear under our breath and curse everyone involved – first our management and clients, then ourselves and our poor coding discipline.
Like any undesirable habit, the habit of not-testing-the-code can be fixed if tests are an integral part of our coding routine, and they get executed and checked without any conscious effort on our part. It looks like similar way of thinking led AngularJS developers to create Karma (formerly Testacular). Their own introduction to Testacular is worth a watch:
[youtube http://www.youtube.com/watch?v=5mHjJ4xf_K0]
So, what does Karma do for you? For day-to-day and hour-to-hour coding, it runs your tests in the background, executing your JavaScript code in real browsers of your choice without you having to run the tests manually. Let me repeat that: you can test your code on Safari, Chrome, Firefox, and Opera, and connect your mobile devices and tablets, and run tests on them as well, automatically, in a matter of fractions of a second.
Thanks to integration with Grunt, every time you modify a file, the test suite gets a re-run, and test results are regenerated. And, when you push your code to your repository, your continuous integration server will pick it up and re-run the full test suite for you.
The official documentation says it best. When should I use Karma?
- You want to test code in real browsers.
- You want to test code in multiple browsers (desktop, mobile, tablets, etc.).
- You want to execute your tests locally during development.
- You want to execute your tests on a continuous integration server.
- You want to execute your tests on every save.
- You love your terminal.
- You don’t want your (testing) life to suck.
- You want to use Istanbul to automagically generate coverage reports.
- You want to use RequireJS for your source files.
How do I write my tests?
It depends on your favourite testing framework. Jasmine, Mocha and QUnit are supported out of the box. If you have another favourite framework, adding an adapter for it to Karma should not be a problem. The ng-boilerplate template provides test examples that use Jasmine.
The best way to learn to write good tests is to read tests of someone who is good at it. And this is where I should introduce an excellent reference app using AngularJS: angular-app. As with ng-boilerplate, this is not an introduction-level tutorial, it’s a real application – both server and client side, CRUD and all.
Unit Tests
Let’s look at a few tests from the application. First a unit test, taken from breadcrumbs.spec.js (note that the directory structure in this app is different from the one suggested by ng-boilerplate). This test checks that the breadcrumbs service (breadcrumbs.js) updates its data when the route changes. For example, if the route of the application changes to “/some/path”, the breadcrumbs should contain the following array:
[{
name: 'some',
path: '/some/'
}, {
name: 'path',
path: '/some/path'
}]
And here’s the complete unit test:
describe('breadcrumbs', function () {
var LocationMock = function (initialPath) {
var pathStr = initialPath || '';
this.path = function (pathArg) {
return pathArg ? pathStr = pathArg : pathStr;
};
};
var $location, $rootScope, breadcrumbs;
beforeEach(module('services.breadcrumbs'));
beforeEach(inject(function($injector) {
breadcrumbs = $injector.get('breadcrumbs');
$rootScope = $injector.get('$rootScope');
$location = $injector.get('$location');
spyOn($location, 'path').andCallFake(new LocationMock().path);
}));
it('should have sensible defaults before route navigation', function() {
expect(breadcrumbs.getAll()).toEqual([]);
expect(breadcrumbs.getFirst()).toEqual({});
});
it('should not expose breadcrumbs before route change success', function () {
$location.path('/some/path');
expect(breadcrumbs.getAll()).toEqual([]);
expect(breadcrumbs.getFirst()).toEqual({});
});
it('should correctly parse $location() after route change success', function () {
$location.path('/some/path');
$rootScope.$broadcast('$routeChangeSuccess', {});
expect(breadcrumbs.getAll()).toEqual([
{ name:'some', path:'/some' },
{ name:'path', path:'/some/path' }
]);
expect(breadcrumbs.getFirst()).toEqual({name:'some', path:'/some'});
});
});
This test is short, but it demonstrates a bunch of important things related to Jasmine and AngularJS testing. If you are new to Jasmine, check out the one-page documentation – you’ll find that describe, beforeEach, expect, and spyOn do exactly what you would expect them to do.
There are a few Angular-specific things here.
- LocationMock emulates the $location service of Angular. It provides read-write interface to the browser’s native window.location.href. Because we are testing the handling of location changes, we want to “navigate to new place”, but we want the test code to talk to our mock object, rather than real browser location.href (which will contain URL specific to the test-runner).
- The calls to module() function tell the test framework the name of the module that has to be loaded for this test.
- The inject() function lets us load dependencies that are normally handled by Angular dependency injector.
- Once we’ve substituted the call to $location with our mock object method, we are ready to start testing.
Integration Tests (a.k.a. end-to-end or e2e tests)
This is where real fun begins. Just have a look at this (from users-edit.scenario.js). This test will load your entire application, log in to admin panel, fills in a new-user form, and verifies that the Save button is enabled. Can you imagine a more succinct way of driving your entire web app and testing for outcome?
describe('admin edit user', function() {
beforeEach(function() {
browser().navigateTo('/admin/users/new');
input('user.email').enter('admin@abc.com');
input('user.password').enter('changeme');
element('button.login').click();
});
it('enables the save button when the user info is filled in correctly', function() {
expect(element('button.save:disabled').count()).toBe(1);
input('user.email').enter('test@app.com');
input('user.lastName').enter('Test');
input('user.firstName').enter('App');
input('user.password').enter('t');
input('password').enter('t');
expect(element('button.save:disabled').count()).toBe(0);
});
});
As I mentioned before, the boilerplate code does not include any e2e tests, just unit tests. So, to include integration tests, you’ll need to add your own.
Integration tests are conventionally saved as .scenario.js files, and we’ll need to update the Grunt file to exclude these scenario files from production build. We’ll use the handling of unit tests (.spec.js files) as an example. The lines in Gruntfile.js that should interest us are:
src: {
js: [ 'src/**/*.js', '!src/**/*.spec.js' ],
...
unit: [ 'src/**/*.spec.js' ]
},
Start working from here and make sure .scenario.js is treated similar to .spec.js. The Gruntfile from angular-app is also a good example – the .scenario.js files are already included in it, and e2e test configuration is included in client/test/config/e2e.js. In addition, the first project template I mentioned in this series – angular-seed – also contains complete configuration for both the unit tests and the end-to-end tests, updated for latest Karma release.
Happy testing!
In the next, final chapter of this overview, I’ll list the common pitfalls to be avoided, best practices to be followed, and specific areas that need our attention as we transition from years of jQuery development into our first well-designed and well-tested large-scale Angular projects.
TL;DR (but really, read the whole three articles – it’s worth it)
- Use ng-boilerplate as a template for your projects.
- Learn to use unit-testing and end-to-end testing with Angular, and make it your religion. Use testacular/karma and Jasmine. It will save you months and years of development. Use end-to-end tests in addition to unit tests. Borrow test practices from good AngularJS applications.
- Acquire new Angular habits, and avoid old habits that do not translate into AngularJS world.
- Know how scopes work
- Learn to use templates, directives and filters instead of DOM manipulation
- Know Angular equivalents of common jQuery functions
- Use AngularUI and related libraries by the same team
- Read good applications sources (https://github.com/angular-app/angular-app/, http://builtwith.angularjs.org/).