Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJs unit testing memory leaks

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

like image 609
S.Klechkovski Avatar asked Oct 07 '15 17:10

S.Klechkovski


1 Answers

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!

like image 177
S.Klechkovski Avatar answered Oct 20 '22 23:10

S.Klechkovski