Simple
Published in

Simple

AngularJS migration — beef up your test cases

How to move fast without breaking things, sorta!

Picture from Tweet by @bradlygreen

Testing JavaScript can be boring, tedious and sometimes time-consuming. It doesn’t have to be that way.

The consensus is quite unanimous when it comes to shipping high-quality well-tested software. Quite surprisingly though, there is a deep divide about how to approach testing!

This article keeps tribalism and judgments aside, and focus on fixing some of the most pressing testing related issues not discussed in other forums.

All articles in these series have an index page

“You ain’t gonna need …” — to read this article, unless at least one of the situations below applies:

  • To save time, some genius figured it was wise to skip some tests — to find out later on that was the worst decision ever made — now you are in charge to turn the ship around
  • You are trusted with legacy large scale migration from AngularJS to Angular — or React — but the poorly tested code make you call in the “green berets” for re-enforcement before you even add one line of code …
  • You are under pressure to fix bugs, add new features while leveling grounds for migration work — whatever the circumstances, you have to deliver on a tight budget, schedule — a production-ready Angular app
  • You experienced first-hand all kinds of worst-case scenarios including some of your customers calling in wondering why that “Proceed to Checkout” button is not “proceeding” anymore!

I apologize to fail you if you are in none of the above situations. There are other related articles on this index.

In case you are planning to migrate to react instead — this writeup will be better aligned with your needs instead — there is also a deep dive into Transitioning(Learn) to React

Now that you know what this is all about, Let’s dive right into it!

You may be wondering why beef up your unit tests if you don’t even have enough time to finish a feature or crush another bug?

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 obviously don’t have the luxury to start from scratch. 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: the ones that are not covered by your test cases yet.

Your recommendation (👏👏) is my motivation to followup with a new article. For any questions leave a comment below, I will be glad to help!

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

Before you start the migration, You have to track these things angular core team put to eternal rest.

  • Standalone Controller
  • DDO(Directive Definition Objects)
  • jqLite
  • angular.module
  • $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

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 the 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 still available 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 with 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 rethink your legacy Angular app in terms of the composition of Components.

Data binding and listeners have changed to include more of one-way data binding. Inter-component data exchange is based on the reactive concept(flux/observable) in detriment of digest cycle — scope change detection.

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 code. A 100% test coverage doesn’t tell the whole story. It is better though to aim for a 100% of legacy code test coverage, before doing any migration exercise to your legacy app.

Unit tests are designed, and should, always run in isolation: that means that there should be zero dependencies from previous tests or states. As a quick example, when your top level component is using various directives, those child-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) test utilities, such as spied upon or mocked objects, in utility libraries. Zero dependency is NOT zero re-usability.

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 ratios. From this point onward, the term “automated test(s)” will refer to unit tests. All apologies if you are an E2E die-hard 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

The question that pops up when it comes to testing is “What to test?”. The short answer is of course — everything. To be more specific though, focus on the 20% that causes you 80% of the problems.

Most troublesome parts are hidden from your test coverage. Take an example of behaviors hidden in your templates. It is easy to fix an obvious bug than a bug deep down into some shady area of your 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! That is how “80% of problems come from 20% of barely tested code”.

JavaScript community has a wide range of variety of tools to choose from when it comes to testing. The good news is you have so many alternatives, the bad news is you are more likely to be infected with “choice paralysis” (a.k.a Analyisis Paralysis).

When you look at it, most tools converge, when it comes to flexibility, advantages, and features. You will need, or already have a/an:

Test Runner — basically the guy who knows how to run your code in a controlled environment, so that you can do mistakes there.

  • karma — dubbed “The Spectacular test runner for JavaScript”. The test runner’s role is to coordinate the players involved in your testing effort! If you tell Karma where your code files are, where your test files are, if you need some fort of browsers, and the kind of reporting tools you have at hand, it will run your tests and provide reports. 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.
  • wallaby(paying).

Testing Framework — they provide BDD(describe/before/afterandit) style or TDD(suite/setup/teardown and test) style utilities.

It is possible to use testing frameworks as test runners as well, most of the arguments are going to be provided either in a configuration file or as command line arguments.

  • jasmine, mocha, and jest have the most vibrant communities around them and are arguably the most adopted. In this category are TAP, AVA, busterJS share the same thing: they come with their own test runners.

Assertion library — testing frameworks may use sugar-coated assertion libraries, for instance, Chai which has should expect and assert assertion utilities

Mocking library — mocking( for instance Sinon: spies, stubs and mocks) libraries.

Reporting tool — : You will also need, or already have, reporting tools: Istanbul is widely adopted with both a CLI, HTML and other formats such as JSON or XML reporting tools.

Browser — Last but not least You will need, or already have a browser. Once again, arguably, Chrome is the most widely used. It used to be a pain in the ass to run Chrome in the headless mode before puppeteer introduction. 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.

Whenever you are not happy, you can always put together your own tools, using one or many components stated above.

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 — or Jest which is less “orthodox” stack in AngularJS world.

Since they all 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 benefits you not worrying much about test runner configuration.

In another hand, Jasmine traded a test runner for an 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 check-out 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 :-)

The structure of a good test case evolves as your code base increases. There is no such a thing as “a blueprint for a killer test case”. Learning and practicing some good practices — will help to improve the way you approach and write test cases.

On the reading side — don’t be afraid to read other people’s test cases available on the public domain, outside your organization. You will always learn a thing or two from those projects — even better, gives you hints on areas you may need to improve your test cases.

Rule of thumb: test cases(test suites), like 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 that 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 get organized, not testing an actual behavior.

# 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>`
});

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 prototype-al inheritance?

Since you are in the refactoring process, testing templates, event handling, timeouts, data bindings and plugins linking to your app will pay-off as you go 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.

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.

In most cases, memory usage increases as the number test case increase, as the size of your codebase grows, worse as a bad decision on when to acquire and release memory in your codebase increase!

Memory leaks in your codebase may end up in your test cases, which in return clogs your test runner. When your tests start taking time to complete, or browsers all the sudden start freezing, the popup of running out of memory alerts, it is probably time you start an investigation to find 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 may help clean up long living or detached DOM objects. There is a paragraph in afterAll discussed below, that can help get going.

On another hand, if retainers have some sort of connection to the $scope(or $rootScope), then it is time you manually clean up references when the scope is destroyed.

Most problems come from the 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.

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 “Spy-ies”.

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 the 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 the 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 a better measure to take.

To clean up memory, use “this” attached to most of the 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 the 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();
});

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();
});

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 has 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 your 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 the 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>

Over time, 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 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 the 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). The lifespan of event listeners attached to the scopes depends on the lifespan of the scope. Likewise, event listeners on Angular compiled node elements depend on the 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 an 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();
}

Testing UI-routes/ngRoutes is not only hard but introduces a couple of 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;
});

WebSockets, like 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 pinpoint, 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
});

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 neighbors. It is green sharing economy, to curb consumerism and save you thousands in credit card debts, and our planet.

--

--

Share your stuff to declutter your space and earn money

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Pascal Maniraho

Web lover, code crafter, beer drinker, created http://hoo.gy, Montrealer, and training to run a half-marathon :-)