AngularJs unit testing memory leaks - angularjs

as you may already know many of us who have a large quantity of written unit test has met with this not trivially solvable problem. I have around 3500+ unit tests written in the Jasmine syntax following the AngularJs unit testing guide. The tests are executed with Karma runner.
The problem is that they cannot be executed all at once due to some memory leaks. While running them the memory builds up no matter on what browser they are runned on and at some point the browser crashes and disconnects. The best workaround that I am aware of by now which is used in the community that have this problem is spliting the tests in multiple runs and at the end get the correct coverage by merging the results from the single runs.
When I first met with this problem I had around 1000 tests. After trying with all the available browsers for running I have split the tests in multiple runs, however it turned out that this is not good workaround for a long time. Now the tests are executed in 14+ single runs that are runned in parallel to reduce the time for completition and still IMO this cannot permanently solve the problem but delay it a litle bit because of resources limitation (RAM, CPU) and annoying time consumption.
Someone can argue that I have memory leaks in my code for which I cannot guarantee even though I don't have any problems alike when running the application in the browser. That is why I have created an example project that will highlight this problem.
There for reproducing this problem I am creating an Angular service which is heavy in memory consumption like this:
app.factory('heavyLoad', function () {
// init
var heavyList = [];
var heavyObject = {};
var heavyString = '';
// populate..
return {
getHeavyList: function () { return heavyList; },
getHeavyObject: function () { return heavyObject; },
getHeavyString: function () { return heavyString; }
};
});
After that I have a simple directive which uses this service to initialize many DOM elements:
app.directive('heavyLoad', function (heavyLoad) {
return {
scope: {},
template: '' +
'<div>' +
' <h1>{{title}}</h1>' +
' <div ng-repeat="item in items">' +
' <div ng-repeat="propData in item">' +
' <p>{{propData}}</p>' +
' </div>' +
' </div>' +
'</div>',
link: function (scope, element) {
scope.items = heavyLoad.getHeavyList();
scope.title = heavyLoad.getHeavyString();
// add data to the element
element.data(heavyLoad.getHeavyList());
}
};
});
And at the end I am dynamically registering 1000 test suites with the test definition for the directive which btw is written as suggested in the Angular unit testing guide.
// define multiple suits with the same definition just for showcase
for (var i = 0; i < 1000; i += 1) {
describe('heavyLoad directive #' + i, testDefinition);
}
To try the example just checkout the project from GitHub and before running karma start run:
$ npm install
$ bower install
I am looking forward to finding where the problem is and solving it finally.
Cheers

The problem was in the forgotten clean-up that needs to be done after each test.
After adding it the number of tests does not matter anymore because the memory consumption is stable and the tests can be run in any browser.
I have added a modification of the previous test definition here that shows the solution with successfully executing 3000 dinamically registered tests.
Here is how the test looks like now:
describe('testSuite', function () {
var suite = {};
beforeEach(module('app'));
beforeEach(inject(function ($rootScope, $compile, heavyLoad) {
suite.$rootScope = $rootScope;
suite.$compile = $compile;
suite.heavyLoad = heavyLoad;
suite.$scope = $rootScope.$new();
spyOn(suite.heavyLoad, 'getHeavyString').and.callThrough();
spyOn(suite.heavyLoad, 'getHeavyObject').and.callThrough();
spyOn(suite.heavyLoad, 'getHeavyList').and.callThrough();
}));
// NOTE: cleanup
afterEach(function () {
// NOTE: prevents DOM elements leak
suite.element.remove();
});
afterAll(function () {
// NOTE: prevents memory leaks because of JavaScript closures created for
// jasmine syntax (beforeEach, afterEach, beforeAll, afterAll, it..).
suite = null;
});
suite.compileDirective = function (template) {
suite.element = suite.$compile(template)(suite.$scope);
suite.directiveScope = suite.element.isolateScope();
suite.directiveController = suite.element.controller('heavyLoad');
};
it('should compile correctly', function () {
// given
var givenTemplate = '<div heavy-load></div>';
// when
suite.compileDirective(givenTemplate);
// then
expect(suite.directiveScope.title).toBeDefined();
expect(suite.directiveScope.items).toBeDefined();
expect(suite.heavyLoad.getHeavyString).toHaveBeenCalled();
expect(suite.heavyLoad.getHeavyList).toHaveBeenCalled();
});
});
There are two things that need to be cleaned-up:
compiled element when using $compile for testing directives
all variables in the describe functions scope
The two of them are tricky and hard to find out and take into consideration.
For the first one I already knew but it didn't helped much until I've discovered the second which is related with how Jasmine works inside.
I have created an issue on their GitHub repository which should help finding better solution or at least spread this information among developers faster.
I hope that this answer will be helpful for lot of people having this problem. I will write some info too after I finish refactoring all my other tests.
Cheers!

Related

Intermittently failing tests with Protractor - promise order execution?

I am facing intermittent protractor test failures across a range of test suites without any real pattern in the fail cases to indicate what could be going on, for example it's not the same tests that are failing. Sometimes I will get many failures and on other occasions just a single fail case.
I should point out that this tends to only happen when performing test runs on a Jenkins CI server we have configured (running under linux). Locally on Windows dev machines we may get a single fail case after 30-40 runs which I can live with!
The application we are testing is currently built with angular 1.5 and we are using angular material 1.1.3
Due to the animations used in angular material and the performance hit these can have, we have already tried disabling animations following this approach here which certainly make the tests quicker but dont help with the fail cases we are seeing/
I am at a point now where I am running one test suite over and over, after 5 successful runs it then failed on it's 6th attempt on our Jenkins CI environment\linux box, locally I have run this test many times now and no failures yet.
The test suite in question is detailed below along with a page object file snippet:
//test suite
describe('Operators View', function () {
var operatorPage = require('./operators.po.js'),
loginView = require('../login/login.po.js'),
page = new operatorPage();
describe('Large screen tests', function () {
beforeAll(function () {
loginView.login();
});
afterAll(function () {
loginView.logout();
});
it('should create an operator', function () {
page.settlementBtn.click();
page.operatorsBtn.click();
page.fabBtn.click();
page.createOperator();
expect(page.headline.getText()).toEqual('Operators');
});
});
});
// operators.po.js
var operatorsSection = function() {
this.helper = new Helpers();
this.headline = element(by.css('.md-headline'));
this.settlementBtn = element(by.css('[ui-sref="settlement"]'));
this.operatorsBtn = element(by.css('[ui-sref="operators"]'));
this.fabBtn = element(by.css('.md-fab'));
// Form Elements
this.licenceNumber = element(by.model('vm.transportOperator.licenceNumber'));
this.tradingName = element(by.model('vm.tradingName'));
this.name = element(by.model('vm.name'));
this.operatorAddressFirstLine = element(by.model('vm.transportOperator.address.line1'));
this.operatorAddressCityTown = element(by.model('vm.transportOperator.address.line5'));
this.operatorAddressPostCode = element(by.model('vm.transportOperator.address.postcode'));
this.payeeAddressFirstLine = element(by.model('vm.transportOperator.payee.address.line1'));
this.payeeAddressCityTown = element(by.model('vm.transportOperator.payee.address.line4'));
this.payeeAddressPostCode = element(by.model('vm.transportOperator.payee.address.postcode'));
this.opID = element(by.model('vm.transportOperator.fields.opID'));
this.spID = element(by.model('vm.transportOperator.fields.spID'));
this.schemeSelect = element(by.model('reference.scheme'));
this.schemeOptions = element(by.exactRepeater('scheme in vm.schemes').row('0'));
this.alias = element(by.model('reference.alias'));
this.reference = element(by.model('reference.reference'));
this.saveBtn = element(by.css('.md-raised'));
this.createOperator = function() {
this.licenceNumber.sendKeys(this.helper.getRandomId(10));
this.tradingName.sendKeys('Protractor Trade Name LTD');
this.name.sendKeys('Protractor Trade Name');
this.operatorAddressFirstLine.sendKeys('Protractor Town');
this.operatorAddressCityTown.sendKeys('Cardiff');
this.operatorAddressPostCode.sendKeys('PT4 4TP');
this.payeeAddressFirstLine.sendKeys('Protractor Town');
this.payeeAddressCityTown.sendKeys('Cardiff');
this.payeeAddressPostCode.sendKeys('PT4 4TP');
this.opID.sendKeys('177');
this.spID.sendKeys('Protractor Spid');
this.schemeSelect.click();
this.schemeOptions.click();
this.alias.sendKeys('PTAlias');
this.reference.sendKeys('Protractor');
this.saveBtn.click();
}
};
module.exports = operatorsSection;
In this test suite after the call to createOperator from the PO file is invoked and the savteBtn is clicked, the application will transition to a state that shows a table of created entries (after successful creation of course). We are using angular ui-router also, currently on version 0.2.18
The expectation fails with:
Expected 'Create An Operator' to equal 'Operators'.
Yet the accompanying screenshot that was captured shows the table view with an 'Operators' heading, it seems the call to page.headline.getText() inside the expectation call is being invoked too soon, so before the database operation to create the item and the page change has had a chance to complete?
I have started wondering if this could be down to the order of promises executed by protractor. I have come across articles talking about control flow in protractor and why there may be occasions when you should hook into the result of a protractor call's promise using .then() - I found this
It got me wondering if I should move the call to my saveBtn.click(), that's called at the end of my page object's createOperator function, into the test suite, so doing something like:
it('should create an operator', function () {
page.settlementBtn.click();
page.operatorsBtn.click();
page.fabBtn.click();
page.createOperator();
page.saveBtn.click().then(function(){
expect(page.headline.getText()).toEqual('Operators');
});
});
I'm starting to clutch at straws here to be honest, so any thoughts\advice from the community here would be much appreciated.
Thanks!
As requested, here is the function I use for waiting for URLs to be as they should.
public waitForUrlToBeLike (urlPart: string, timeout: number = 10000) {
return browser.wait(() => {
return browser.driver.getCurrentUrl().then((url) => {
let regex = new RegExp(urlPart);
return regex.test(url);
});
}, timeout);
}
I also use the following a lot to wait for elements to be present before making assertions on them:
public waitTillPresent (element: ElementFinder, timeout: number = 10000) {
return browser.wait(() => {
return element.isPresent();
}, timeout);
}

Karma/Jasmine directive testing dom compiles but can't access it

I have been trying to figure out a way to test the focusElement function of my directive. But somehow, whenever I call the function in my test, the classElements variable is undefined. Anybody have a clue?
Here is the directive function
$scope.focusElement = function() {
if (attrs.focusToClass) {
$scope.classElements = angular.element(document.querySelectorAll('.' + attrs.focusToClass));
if (attrs.focusToClassIndex) {
// Focus to the class with the specified index.
// Index should be within the length of elements and must not be a negative number.
if (attrs.focusToClassIndex < $scope.classElements.length && attrs.focusToClassIndex >= 0) {
$scope.elementToFocus = $scope.classElements[attrs.focusToClassIndex];
}
} else {
// Goes to the first index if the index is not specified.
$scope.elementToFocus = $scope.classElements[0];
}
} else if (attrs.focusToId) {
// Focus to the given ID
$scope.elementToFocus = angular.element(document.querySelector('#' + attrs.focusToId))[0];
}
if ($scope.elementToFocus) {
$scope.elementToFocus.focus();
}
}
Here is the unit test code.
describe('focusElement function', function () {
var targetElement;
var element;
var elementScope;
var elementToFocus;
beforeEach(function() {
targetElement = $compile('<div id="targetDiv" class="targetClass"><span id="targetSpan" class="targetClass"></span></div>')($rootScope);
$rootScope.$apply();
});
it('should return the class element with index 0', function() {
element = $compile('<div next-focus focus-to-class="targetClass"></div>')($rootScope);
});
it('should return the class element with the specific index within the range', function() {
element = $compile('<div next-focus focus-to-class="targetClass" focus-to-class-index="1"></div>')($rootScope);
});
it('should return the class element with the specific index outside the range', function() {
element = $compile('<div next-focus focus-to-class="targetClass" focus-to-class-index="-1"></div>')($rootScope);
});
it('should return the id element', function() {
element = $compile('<div next-focus focus-to-id="targetDiv"></div>')($rootScope);
});
afterEach(function() {
elementScope = element.scope();
spyOn(elementScope, 'focusElement').and.callThrough();
elementScope.focusElement();
console.log(elementScope.classElements);
expect(elementScope.focusElement).toHaveBeenCalled();
expect(elementScope.elementToFocus).toBeDefined();
expect(elementScope.elementToFocus.focus).toHaveBeenCalled();
});
});
Here is the error
The error is the result of you using document directly in the code which comprises the unit under test. The solution is to refactor your code to not use document directly, rather use jQuery style $() syntax to get much the same behaviour, make the context which $() operates an injectable dependency and then in your unit test use things like fixtures to inject a well-known context during test. Since you are using Jasmine already, you probably want to look into jasmine-jquery for convenient API to do this easily.
(Alternatively, in this specific case you could also setup a stub/mock document.querySelectorAll in your beforeEach() callback.)
That's the where-does-the-issue-stem-from and the how-to-fix-it (high level) but it pays to understand Karma a bit better before proceeding.
Skipping over lots of finer points, basically karma consists of three things combined into one single application:
An extensible 'dummy' HTTP server to serve up content (as configured in files). This server is structured as an Express JS app, which is useful to remember if you ever want to integrate custom middleware. E.g. to expose additional paths on the server in order to, say, provide a dummy API server for your Angular app code to interact with. A particular path to remember is '/base' which corresponds to the project directory as defined in your Karma config (the working directory).
A driver to point a browser to a synthesied dummy 'index.html' kind of page on the HTTP server (which is how all entries in files for which included: true are loaded, basically as <script> tags).
An API/framework for integrating unit test logic, reporting, etc. This is where the karma-jasmine type plugins interact and also how Karma is able to get the output back out and determine whether tests succeeded or not.
Points #2 and #3 have implications, in particular that the environment (window) and the HTML (document) should essentially be treated as an internal implementation detail of Karma and are not to be relied on during tests. So that means your code under test also should not rely on these details either. This is why you should restructure your code to pass in the context for $() as a dependency and then you can pass a well-known fixture in your tests and the normal context in your actual app.

Angular: Get aware of spelling mistakes in function call with ng-click

My question is about discovering possible spelling mistakes in angular expressions, in particular spelling mistakes in the function name.
Consider the snippet bellow:
I have two buttons there, the first one with correct spelling, the second with a spelling mistake in the angular expression. Clicking the second button does nothing and gives no hints about a potential error.
My question is now: are there ways to detect erroneous calls to function that don't exist (while executing the application)?
I am not looking for some checking possibility in the build or unit test process but rather would like to see a way I could get aware of such a potential issue when running the erroneous expression in the browser when the application is executed.
angular.module("myApp", [])
.controller("TestController", function($scope){
$scope.myFunction = function() {
console.log("Hello World");
};
});
angular.element(document).ready(function () {
angular.bootstrap(document, ['myApp']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
<section ng-controller="TestController">
<button ng-click="myFunction()">myFunction</button>
<button ng-click="myFunctio()">myFunctio</button>
</section>
I'm not familiar with a built in option in angular to do that (using binding to an "undefined" object is a legit UC as things may become "undefined" during program run) - but you may write your own "ng-click" directive which, in case not finding the function to bound to, raise an error (exception or better - console error / warning).
This is an extremely common complaint about Angular. Even when writing code for the Closure compiler, with all the type annotations and everything, these still fall right through the cracks.
You can kluge something together, I've seen things like bussing all events to a common broker and looking for the target handler in the bound scope, and so on. But it always appears to be more trouble than it's worth.
Your unit tests are where you catch this sort of thing. It's why being able to test template code via triggering events is such an important thing for an Angular developer to master. If you trigger that button click and your test fails (e.g. your spyOn the handler never gets called), check the template.
Protractor (and other end to end testing frameworks) will do that for you.
I'm not sure if this would work for function calls or not, but it would solve part of the problem of misspelling something. In Scott Allen's AngularJS Playbook course on Pluralsight, he suggests creating a decorator for the $interpolate service to see if any bindings are potentially incorrect. Here is the code for that:
(function(module) {
module.config(function ($provide) {
$provide.decorator("$interpolate", function ($delegate, $log) {
var serviceWrapper = function () {
var bindingFn = $delegate.apply(this, arguments);
if (angular.isFunction(bindingFn) && arguments[0]) {
return bindingWrapper(bindingFn, arguments[0].trim());
}
return bindingFn;
};
var bindingWrapper = function (bindingFn, bindingExpression) {
return function () {
var result = bindingFn.apply(this, arguments);
var trimmedResult = result.trim();
var log = trimmedResult ? $log.info : $log.warn;
log.call($log, bindingExpression + " = " + trimmedResult);
return result;
};
};
angular.extend(serviceWrapper, $delegate);
return serviceWrapper;
});
});
}(angular.module("common")));

Is Jasmine advise to reduce local variable or karma coverage report?

My Karma coverage report shows to cover the local variable. is that possible or its a karma-coverage report issue.
Please have a look at the Angular Controller Code.
'use strict';
angular.module('moduleName')
.controller('FormController', [ '$log',
function FormController($log) {
// Controller Local Variables.
var that = this;
that.hideLoader = function () {
that.isLoading = false;
};
}
]);
My Question: Is that possible to cover the local variables and function parameter conditions. for instance is below.
that.hideLoader = function (userObj) {
var id = userObj.id;
if(id) {
that.isLoading = false;
}
else {
that.isError = true;
}
};
In the above example, I have declared user object id attribute to local id variable. now its very tough to cover the code. in this case, jasmine advise to reduce local variable or its karma-coverage report suggestion ?
my karma coverage report wants to cover the local variable. is that
possible or its a karma-coverage report issue.
The coverage tool is working correctly by checking that your tests cover every line of code. That is the definition of code coverage.
On the other hand:
var that = this;
that.hideLoader = function() { that.isLoading = false};
are NOT local variables. As defined, they are properties of your controller. Word of caution: please use "use strict" and don't use undeclared properties such as that.isLoading. It is not very readable and it is bad practice even though the language allows for it.
Also, when asking questions please paste the code and not images of the code.
Update
The answer to your question is yes. Karma-coverage reports on every line of code touched (green), or untouched (red).

Angularjs Passing array between controllers

I have been through several tutorials and posts about this topic and still can't seem to figure out what is wrong with my code. To me it seems I am having scoping issues with the data within my service. My code is split up into separate files. Here is my code:
github link : https://github.com/StudentJoeyJMStudios/PetPinterest.git
//in dataService.js
var app = angular.module('se165PetPinterestWebApp');
app.service('SharedData', function ()
{
var categoryOfSelectedAnimals = [];
this.setCatOfSelAnimals = function(pulledCategoriesFromParse)
{
categoryOfSelectedAnimals = pulledCategoriesFromParse;
console.log('after assignment in set::' + categoryOfSelectedAnimals);
};
this.getCatOfSelAnimals = function()
{
console.log('in get::::' + categoryOfSelectedAnimals);
return categoryOfSelectedAnimals;
};
});
in my first controller to set the data in signup.js
app.controller('SignupCtrl',['$scope', 'SharedData', function ($scope, SharedData)
{
var Categories = Parse.Object.extend('Categories');
var query = new Parse.Query(Categories);
query.find({
success: function(results)
{
$scope.availableCategoriesOfAnimals = results;
SharedData.setCatOfSelAnimals(results);
},
error: function(error)
{
alert('Error: ' + error.code + ' ' + error.message);
}
});
};
}]);
Then in my other controller trying to get the data from the array within my service:
var app = angular.module('se165PetPinterestWebApp');
app.controller('CatSelCtrl', function ($scope, SharedData)
{
$scope.availableCategoriesOfAnimals = SharedData.getCatOfSelAnimals();
});
When I print the contents from the SharedData.getCatOfSelAnimals I get 0 every time. Please help. Thank you very much in advance.
EDIT: After playing around with a string a bit I am finding the changed string in the set function is not saved into the service and when I call my get function within my service the string is not changed from the set method. Please help, thank you in advance.
EDIT: So it looks like when I navigate to new page by using window.location.href = '../views/categorySelection.html'; in my signup.js it reloads my dataService.js which re-sets my variables back to nothing. Does anyone have any ideas as to how to fix this?
Edit
First: why you lose data
You need to setup routing properly. Right now you are not changing views but rather using window.location.href to load a new bootstrap file (dashboard.html), i.e. everything saved in memory will be lost. So you have been doing it right, sort of, but the moment you change to dashboard.html all data from Parse is lost.
You can solve this by configuring routes and use $location.url() to change URL. Read more about angular.route here: https://docs.angularjs.org/api/ngRoute/service/$route
The angular way
After looking at your code and running your app I think we need to take a step back. Angular is tricky to get used to but there is a lot of good tutorials. I think you might wanna read some of them to get a better grasp of how it works and how to setup and build your app.
Start here: http://www.airpair.com/angularjs
Boilerplate
Found this boilerplate for an Angular app using Parse. It might be something you could use. https://github.com/brandid/parse-angular-demo
Original
Or an even quicker way to empty $scope.availableCategoriesOfAnimals and then merge new data without breaking reference:
$scope.availableCategoriesOfAnimals.length = 0;
Array.prototype.push.apply($scope.availableCategoriesOfAnimals, pulledCategoriesFromParse);
You are breaking the reference on assignment. This is a JavaScript issue, not an angular one for that matter ;)
Try this in your set function:
categoryOfSelectedAnimals.length=0;
pulledCategoriesFromParse.forEach(function (e) {categoryOfSelectedAnimals.push(e)});
in stead of reassigning
edit: angular extend works on objects, not arrays, so replaced it with a bit of JS.

Resources