AngularJs单元测试内存泄漏

AngularJs单元测试内存泄漏,angularjs,unit-testing,memory-leaks,jasmine,karma-runner,Angularjs,Unit Testing,Memory Leaks,Jasmine,Karma Runner,正如您可能已经知道的,我们中有很多人都有大量的书面单元测试,他们遇到了这个不容易解决的问题。我有大约3500多个单元测试是按照AngularJs指南的语法编写的。使用runner执行测试 问题是,由于一些内存泄漏,它们不能一次全部执行。在运行它们时,无论它们运行在哪个浏览器上,内存都会累积,并且在某个时候浏览器会崩溃和断开连接。到目前为止,我所知道的最好的解决方法是在有此问题的社区中使用,将测试拆分为多个运行,最后通过合并单个运行的结果来获得正确的覆盖率 当我第一次遇到这个问题时,我进行了大约1

正如您可能已经知道的,我们中有很多人都有大量的书面单元测试,他们遇到了这个不容易解决的问题。我有大约3500多个单元测试是按照AngularJs指南的语法编写的。使用runner执行测试

问题是,由于一些内存泄漏,它们不能一次全部执行。在运行它们时,无论它们运行在哪个浏览器上,内存都会累积,并且在某个时候浏览器会崩溃和断开连接。到目前为止,我所知道的最好的解决方法是在有此问题的社区中使用,将测试拆分为多个运行,最后通过合并单个运行的结果来获得正确的覆盖率

当我第一次遇到这个问题时,我进行了大约1000次测试。在尝试使用所有可用的浏览器运行之后,我将测试分为多个运行,但是很长一段时间以来,这并不是一个好的解决方法。现在,测试在14次以上的单次运行中执行,这些运行是并行运行的,以减少完成时间,但在我看来,这无法永久解决问题,但由于资源限制(RAM、CPU)和恼人的时间消耗,会稍微延迟一点

有人可能会争辩说,我的代码中存在内存泄漏,对此我无法保证,即使我在浏览器中运行应用程序时没有任何问题。这就是为什么我创建了一个示例项目来突出这个问题

为了重现这个问题,我创建了一个角度,它消耗了大量内存,如下所示:

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; }
  };
});
之后,我有一个简单的示例,它使用此服务初始化许多DOM元素:

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());
    }
  };
});
我期待着找到问题所在并最终解决它


干杯

问题在于每次测试后需要进行的遗忘清理。 添加后,测试的数量不再重要,因为内存消耗是稳定的,测试可以在任何浏览器中运行

我已经添加了对先前测试定义的修改,它显示了成功执行3000个动态注册测试的解决方案

下面是测试现在的样子:

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

});
description('testSuite',函数(){
var-suite={};
在每个(模块(“应用”)之前;
beforeach(注入函数($rootScope,$compile,heavyLoad){
套件。$rootScope=$rootScope;
套件。$compile=$compile;
suite.heavyLoad=heavyLoad;
suite.$scope=$rootScope.$new();
spyOn(suite.heavyLoad,'getHeavyString')。和.callThrough();
spyOn(suite.heavyLoad,'getHeavyObject')和.callThrough();
spyOn(suite.heavyLoad,'getHeavyList')和.callThrough();
}));
//注:清理
在每个(函数()之后){
//注意:防止DOM元素泄漏
element.remove();
});
毕竟(功能){
//注意:防止由于为创建JavaScript闭包而导致内存泄漏
//jasmine语法(beforeach,afterEach,beforeAll,aftereall,it.)。
suite=null;
});
suite.compileDirective=函数(模板){
suite.element=suite.$compile(模板)(suite.$scope);
suite.directiveScope=suite.element.isolateScope();
suite.directiveController=suite.element.controller('heavyLoad');
};
它('应该正确编译',函数(){
//给定
var givenTemplate='';
//什么时候
compileDirective(givenTemplate);
//然后
expect(suite.directiveScope.title).toBeDefined();
expect(suite.directiveScope.items).toBeDefined();
expect(suite.heavyLoad.getHeavyString).tohavebeincall();
expect(suite.heavyLoad.getHeavyList).tohavebeincall();
});
});
有两件事需要清理:

  • 使用$compile进行测试指令时的compiled元素
  • 描述函数范围中的所有变量
这两个问题都很棘手,很难找到并加以考虑。 对于第一个,我已经知道了,但是直到我发现了第二个,它与茉莉花在体内的工作方式有关,它才起到很大的作用。 我已经在他们的GitHub存储库上创建了一个应用程序,它应该有助于找到更好的解决方案,或者至少可以更快地在开发人员之间传播这些信息

我希望这个答案会对很多有这个问题的人有所帮助。在我完成重构所有其他测试之后,我也会写一些信息


干杯

成功重构后,约2分钟内执行约4000个测试。您是如何做到这一点的?我已经使用
this
为Jasmine的清理设置了以前的测试,但是在运行了600个测试之后,运行程序在PhantomJS上失败了。同样的情况也会发生,但在Chrome上运行的测试更少。除此之外,从1.5.1开始,您可以使用
beforeAll
而不是
beforeach
来模拟每个模块一次,这样可以进一步提高性能@维瓦斯科谢谢你,我直到现在才意识到这一点。是的,这会稍微提高性能,因为$injector不会在每次测试之前重新创建,但应该小心使用,因为某些服务有时会保持某些状态,这可能会影响测试用例。@S.Klechkovski感谢您的这篇文章!所有
$compiled
元素上的Karma+mocha和元素都有相同的问题。remove()修复了我的问题!
$ npm install
$ bower install
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();
    });

});