AngularJS migration — beef up your test cases

How to move fast without breaking things, sorta!

Picture from Tweet by @bradlygreen

Intro. With all pressure to deliver working Angular app on time, at one point, you may have thought to abandon your automated tests to save some time. Now, with the shire size of your app, you cross fingers before you make even a smallest modification because, well, things break. Worst case, your customer is first person to notice that the “Checkout button” is not working anymore! If you are in a similar situation, read on.

Nota: this is the second in a series of blog posts about soft code migration, as I move Hoo.gy — a platform that helps you to rent various stuff from your friends, neighbours and coworkersfrom Angular 1.x to Angular 4.x. Your recommendation is my motivation to followup with a new article. For any questions leave a comment below, will be glad to help! I hope this helps you!

Since these blog series are about code migration, let’s suppose that you are trusted to migrate your large scale app, of poorly tested, legacy Angular to Angular(2 or 4.x). As always, you are on tight schedule and budget. You are required to add more features and fix a couple bugs in your backlog as you go, same old s* just different days. If by any chance, anything breaks … well, the whole team will proceed with your execution! Harsh.


Why. Why beef up your unit tests if you don’t even have enough time to finish a feature on time? Let’s just start by saying that those tests are your best friend who cares enough to let you know whenever you mess up with the famous “Checkout button”. Makes sense!

…This ship has been sailing for years and has never sunk — let’s throw away the lifeboats!~ peterclary

You are lucky if you can rewrite the whole app from scratch! Obviously, you don’t have a luxury to do that. A disciplined refactoring will help you add value to your code, to avoid fixing same bug overtime, and help you think twice as you move forward. Automated tests boost your confidence that new modifications introduce relatively fewer bugs, and those are not covered by any test yet.


Objective. The point of this blog is to highlight a couple things hard to test, so you are aware of those while you migrate your large scale legacy Angular app. It challenges you to harden and revamp your tests to consume less memory, fight memory leaks, and to reduce time to run large test cases. It helps you reconsider making small tested deploy-able changes, as you modernize your large scale angular app, in an environment yielding an urgency to modernize your legacy app, but resources(time, money and manpower) to rewrite the whole thing are quite scarce. This blog does NOT tell you how to get started with TDD/BDD. It supposes that you are quite comfortable to navigate JavaScript TDD nitty gritty and already adopted a testing strategy within your organization.

…They decided (the single worst strategic mistake that any software company can make) to rewrite the code from scratch ~ Joel On Software

RIP. Before you start migration, You have to track these things angular core team put to eternal rest. Standalone Controller, DDO(Directive Definition Objects), jqLite, angular.module and $scope, and all things that depend on $scope. Yeah! no more scope soup. If deep down you think that’s way fucked up, you are completely right! Dude, that is a lot of changes to do.

Source: last presentation at NG Europe — source

Re-purposed. As you may have noticed already, the directive concept has been split into two distinct but quite similar concepts: Component and Directive. Even though a Component looks and works almost like a special kind of Directive, You will be forced to rethink architecture of your frontend code. The big difference is DOM manipulation is reserved to Directives, as in legacy Angular. The Controller, as a concept, is available to to both Components and Directive. For a smooth transition, You may consider your old Controllers to be de facto smart Components. The way, Controller actions, functions associated to a route, become de facto dumb Components. The template changed in look with augmented superpowers(behaviors). In his “Refactoring Angular Apps to Component style” blog post, Tero Parviainen (@teropa) made a good guide on how to think your legacy Angular app in terms of composition of Components. Data binding and listeners have changed to include more of one-way data binding. Inter-component data exchange is based on reactive concept(flux/observable) in detriment of digest cycle(scope change detection).


Strategy. Divide et impera(divide and conquer). Since BDD is still TDD, choose whatever works for you better! Test coverage is one of metrics to evaluate your code overall health. Battle tested apps proven that 100% test coverage is no synonym to bug free. Even if 100% test coverage doesn’t tell the whole story. It is better you cover 100% of legacy code, before doing any modification to your legacy app. Unit tests should run always in isolation: no dependency from previous tests or states. If your top level component is using various directives, those directives should be tested outside the top level component test case. Same applies to Services used by the said top level component. Group re-usable components, such as spied upon or mocked objects, in utility libraries.


Clean test cases. For practical reasons, You may limit automated tests to Unit Tests first. Just say no more E2E tests. You can skip them, for now or keep 80 UT to 20 E2E ratio. From this point onward, the term “automated test(s)” will refer to unit tests. All apologies if you are an E2E diehard fan.

Code rots and smell, so do unit tests. While refactoring your code, think of refactoring your test suites as well. This becomes automatic when you adopt “write failing tests” first, then “write some code” and “start over” strategy.

Awesome illustration(used without author’s authorization) — via Albert Salim’s blogpost — original author may be this Nat Price

What. Quick answer to “What to test” question is: hard to test parts. It is easy to fix an obvious bug than a bug deep down into some shady area of your code. Most troublesome parts are hidden from your test coverage. Take an example of templates. 80% of problems come from 20% of barely tested code. Examples being templates, event handling or third party plugins. If you do not smoke test(UI/integration tests), chances are you will notice UI discrepancies while in production!

While migrating your code, how confident are you, that a replacement didn’t break anything in your templates? Or, how can you make sure that models are still available and usable in templates after adopting a “ControllerAs” notation? How can you make sure that while eliminating scope soup, data transfer between directives and components is still available and accurate, given the fact that scope uses prototypal inheritance.

Since you are in refactoring process, testing templates, event handling, timeouts, data bindings and plugins linkings to your app will payoff as you move forward with migration. Although the consensus on UI testing tends to defer these things to E2E tests, you will be better off having basic tests that cover templates, event handling, data transfer et al., even though such test scenarios may not be reported in test coverage.


Tools. JavaScript community has a wide range of varieties to choose from. The good part is you have so many choices. The ugly part is You have many parameters to navigate before you make a final decision. When you look at it, most tools converge, when it comes to flexibility, advantages and features. You will need, or already have, a test runner: karma, or wallaby(paying). You will need, or already have, a testing framework: jasmine, mocha and jest(recent) are arguably the most adopted, powered by a vibrant community. Testing frameworks may use sugar coated libraries for assertions(for instance Chai: should, expect and assert) or mocking( for instance Sinon :spies, stubs and mocks) libraries. In this category are tap, AVA, busterJS and a lot more. You will also need, or already have, reporting tools: Istanbul is widely adopted with both a CLI and HTML reporting tools. Karma evolved from Vojta Jína(@vojtajina)’s Master’s Research project back in 2013, caught JavaScript community off guard and has ever since been its hostage. Last but not least You will need, or already have, browsers. Once again, arguably, Chrome. It is pain in ass to run Chrome in headless mode. PhantomJS and jsdom may be widely adopted for good and odd reasons! Just because PhantomJS is headless doesn’t mean it will run faster, quite contrary on larger codebases.


Stack. The combo Karma/Jasmine/Chrome|PhantomJS/ngMock is arguably most adopted in Angular community. Hackers may be more into Mocha/Chai/Sinon/jsdom|xHeadLessBrowser/ngMock, Jest or less “orthodox” stacks. Since they operate in similar fashion, adoption of one stack over another is mostly based on taste, allegiance, performance or combination of both. As an example, Mocha comes with built in test runner. Its adoption benefit you not worrying much about test runner configuration. In another hand, Jasmine traded a test runner for exquisite assertion, spy and mock libraries. You are on your own to choose your test runner! If you need another perspective and reason why you may consider(or migrate your current stack). Two readings you should checkout whenever you are free: “From Karma to Mocha, with a taste of jsdom” and Eric Elliott’s “Why I use Tape Instead of Mocha & So Should You” can help you get started.

Mocha/Chai/Sinon/ngMock stack. Image(published without permission) from Jason Watmore blog
Karma/Jasmine/ngMock stack. — mixed drawing I made :-)

Structure. The structure of a good test case evolves as your code base increases. There is no such as thing as a blueprint of a killer test case, but some basic good manners, or reading others test cases can give you hints on how to improve yours. Rule of thumb: test cases(test suites), as any other piece of code, should be eloquent.

el·o·quent: / ˈeləkwənt/ fluent or persuasive in speaking or writing. source

To better organize my tests, I go with GivenWhenThen. From those behaviour you can easily derive test cases(suite) especially for E2E test cases. Since unit tests should be independent, adopting the same strategy will only help you getting organized, not testing an actual behaviour.

# Modified example from "On Developer Testing"
GIVEN an authenticated user
WHEN user submits inquiry form
THEN inquiry email is sent to info@acme.com
AND a copy is stored in inquiries database
describe('Inquiry', function(){
beforeEach(function(){
#GIVEN authenticated user
this.$scope.user = UserService.get(token);
this.elt = angular.element('<inquiry/>');
this.tpl = this.$compile(this.elt)(this.$scope);
this.controller('inquiry');
});
 it('should submit inquiry form',function(){
spyOn(this.EmailService, 'sendEnquiry').and.callThrough();
spyOn(this.DatabaseService, 'persistEnquiry').and.callThrough();

# WHEN user submits inquiry
this.controller.sendEnquiry();
this.$httpBackend.flush(); # force http request
this.$timeout.flush(); #force flush promises(hack)

#THEN enquiry email is sent
expect(this.EmailService.sendEnquiry).toHaveBeenCalled();

# AND a copy stored in inquiries database
expect(this.DatabaseService.persistEnquiry).toHaveBeenCalled();
  # close connections + restore spies
});
});
# Inquiry component may look like:
angular
.module('awesome')
.component('inquiry',{
controller: function(){
this.sendEnquiry = function(){
EmailService
.sendEnquiry(this.inquiry)
.then(DatabaseService.persistEnquiry)
.then(function(response){
#display success message, etc.
});
};
},
template:
`<form>
<input/><button ng-click='sendEnquiry()'/>
</form>`
});

Memory leaks. The culmination of bad memory management is when the browser dies! It is not easy to run 1k+ test scenarios, let alone test suites. In any case, running out of memory is the worst nightmare you can ever wish to have. Memory usage increases as test cases increase, as size of your codebase grows, worse as bad decision on when to acquire and release memory in your codebase increase! Memory leaks in your codebase transit in your test cases, and end up clogging your test runner. When your tests start becoming lazy to complete, or browsers all the sudden start freezing, popup of running out of memory alerts, it is time you start finding the culprit, and refactor both your test cases and code for better performance. Often memory leaks are closely related to detached DOM trees. Tear down phase helps cleanup any long living or detached DOM objects. There is a paragraph in afterAll discussed below, that can help get going. If retainers has some sort of connection to the $scope, then it is time you manually cleanup references when the scope is destroyed. Most problems comes from inability of the framework to clean up long living objects(due to scope soup may be!), human errors and test runner (bugs) failing to release large objects between test suites or pages.


Mocking. If your code has intensive API calls, grouping mocked objects may help you reduce code repetition. Grouping may help you to DRY your test cases. If necessary, mocked objects can be unified and assembled into their own library(fixtures). Same applies to Spyies.


Setup. Fast set up saves time on overall performance. In TBB, initialization code block used in beforeEach close, runs for every test case. Lazy loading, Memoizing, mocking external systems, reducing initialization blocks, or reduce the number of Injectable blocks can help to reduce overall performance of tests. Minutes are expensive, if you run 1000 times a day, those are 1000 minutes you will need to wait before you see test results.

Most of time is spent in beforeEach(inject(fn)). Reducing the number of Injections helps a lot. Angular provides a quick and dirty tip save resources by using a shared injector. Mocking most of networks related objects(Stripe, Facebook/Google/Twitter Auth objects) reduces memory usage, prevents from hitting third party servers while testing and helps to run large test cases in small time. Tracking which tests takes more time will help you isolate reasons why some tests are slow, and helps you figure out proper measure to take.

To cleanup memory, use “this” attached to most of objects. That way, object transfer between test cases is managed by Jasmine, and help to process faster(viewable on PhantomJS browser). While investigating memory issues in test cases, I stumbled upon these two articles that may change the way you structure your test cases too: Better Jasmine Tests with “this” and Avoid Memory Leaks in AngularJS Unit Tests. They are good to know how to improve overall performance of your tests.

beforeEach(function(){
#testing users component|directive

this.element = angular.element('users-card');
this.template = this.$compile(this.element)(this.$scope);
this.controller = this.element.controller('usersCard');
this.$scope.$digest();
this.scope = this.element.isolateScope()
|| this.element.scope();

#Force root level digest: expensive
this.$rootScope.$digest();
});

Teardown. Application’s increase is LoC affects directly resources needed to test it. Resources being test cases and memory to run test cases. To better manage memory, a cleanup phase destroys objects resting in memory. If you read most bug reports in automated testing tools, memory and other weird problems relating to starvation are common. To avoid memory leaks, always remember to flush any pending HTTP request, clear timeouts, deallocate retained objects, close any open connection(such as WebSocket, Database, etc).

#@requires deallocate utility
#@requires flushhttp utility
#@requires clearTimeouts utity
afterEach(function(){
deallocate(this.template); #removes DOM element in memory
deallocate(this.element); #removes DOM element in memory
flushhttp(this.$httpBackend); #reminder to flush pending requests
clearTimeouts(); #deals with timeout related memory leaks
});
#avoid $ related detached DOM trees. 
#$ cache may hold reference to DOM trees.
afterAll(function(){
$.removeData();
});

Test “hard to test parts”. Link constructs, template embedded actions, event handling, bindings etc. The reason you should include template tests templates, even if they are not included in test coverage reports is, in part, template notation have changed dramatically from $scope bound functions to controllerAs notation and most recently to constructs available within templates(ng-click, ng-mouse-X, loops such as ng-repeat, and condition statement ng-if). Although template testing is a part of E2E testing, including changing templates testing parts covers you first hand.

# test case with $scope soup.
it('should have ng- directives in templates',function(){
expect(this.elt.html()).toContain('ng-repreat="user as users");
expect(this.elt.html()).toContain('ng-if="users.length > 10"');
expect(this.elt.html()).toContain('ng-click="details(user)"');
});
#can be used to test
<div ng-init="init()">
<card ng-repeat="user as users" ng-click="details(user)"></card>
<div ng-if="users.length > 10" ng-click="more()">More</div>
</div>
</div>

The above test can be used to verify implementation of ControllerAs notation:

# test case with reduced scope soup
it('should have ng- directives in templates',function(){
expect(this.elt.html()).toContain('ng-init="vm.init()");
expect(this.elt.html()).toContain('ng-repreat="user as vm.users");
expect(this.elt.html()).toContain('ng-if="vm.users.length > 10"');
expect(this.elt.html()).toContain('ng-click="vm.details(user)"');
});
#updated for controllerAs notation
<div ng-init="vm.init()">
<card ng-repeat="user as vm.users" ng-click="vm.details(user)"></card>
<div ng-if="vm.users.length > 10" ng-click="vm.more()">More</div>
</div>
</div>

Destroying objects in Link function. Overtime, If your stack is Karma/Jasmine/Phantom , then you have to take into account inability of PhantomJS to clean objects between test cases, test suites or pages. Angular 1.x handles objects cleanups after use, except some that you have to remove yourself, and most of them are on Link function. According to Angular documentation, $destroy event is emitted when a DOM node compiled by NG’s compiler is destroyed. Also, $destroy event is broadcast to listening children scopes when a parent scope is destroyed. You have to notice that these two events are operating in two separate contexts, which make you manually handle $destroy event on both element and $scope.

link: function($scope, $element, $attrs, $ctrl){
 function cleanup(){
if(angular.isElement(this)
# .off() events attached to children of current $element;
# trigger $scope.$destroy
else
# deregister $watches, .$on events
# cancel $timeouts and $intervals
}
 $scope.$on('$destroy', cleanup);
$element.on('$destroy', cleanup);
}

Some of code blocks suspected to retain references after DOM node or scope has been destroyed are: $watch, $timeout, $interval. In addition to that there are jQuery, jquery lite and scope event listeners ($rootScope.$on, $scope.$on, element.on, $(identifier).on). Lifespan of event listeners attached to the scopes depends on lifespan of the scope. Likewise, event listeners on Angular compiled node elements depends on lifespan of the node itself. Listeners registered to other objects(including Services) are not cleaned. To avoid memory leak, un-cleaned listeners should be cleaned manually. How? jQuery/lite’s element.remove() removes element from DOM and only element.on handlers. element.remove() should make sure to trigger $scope.$destroy.

Remove long living objects after each test-case as available in angular.mock tear-down function.

#https://goo.gl/Qiw9HH
if (injector) {
injector.get('$rootElement').off();
injector.get('$rootScope').$destroy();
}

ngRoutes/ui-routes. Testing ui-routes/ngRoutes is not only hard, but introduces a couple problems. For instance when your test runner tries to reload a page, you may encounter an error(or warning) “Some of your tests did a full page reload!”. Given a legacy Angular app that leverages ui-router’s states. The solution may be to spy and fake function calls that involve page redirections or reloads.

#somewhere in a controller, service or helper
this.redirectToHome = function(){
window.location.href = 'some/path';
};
# in setUp(beforeEach()) block
spyOn(Ctrl, 'redirectToPage').and.callFake(function(args){
# log event, return a promise, or boolean
return true;
});

WebSocket. WebSockets, as other expensive resources(HTTP request, file system or database connections) , should be mocked to save time. Among many other libraries, Mock Socket can help you. It is also possible to leverage Jasmine’s mocking and spying capabilities with extra little hacks! Jasmine makes it possible to mock all variables using Object Mocks. If you are a big fan of Sinon, go for it! Some open WebSocket connections may be hard to pin-point, subsequently drain memory while testing. It is advised to close any open connection while tearing down your test suite.

# test suite tear down
afterAll(function(){
this.ws.close(); #closing websocket if not mocked
$.removeData(); #removing jQuery's cached data
this.$rootScope.$destroy(); #if deemed necessary
});


Reading list: Headless Chrome.

Reading list — memory leaks in AngularJS

Reading list — testing angular parts

Reading list — classics on memory leak and management

There is a lot writings around the web, but following are the classics, you should check them out:


Gosh, You rock If you read all above goodness! Thank You! Like last time, YOUR recommendation motivates me to follow up this post with techniques I use while upgrading Hoo.gy — a platform that makes it possible for you to rent stuff from your friends and neighbours. It is green sharing economy, to curb consumerism and save you thousands in credit card debts, and our planet;-).