Углеродный блок тестирует утечки памяти

как вы уже знаете, многие из нас, у которых есть большое количество написанных unit test, встретились с этой проблемой, которая не является тривиально разрешимой. У меня примерно 3500+ единиц тестов, написанных в синтаксисе Jasmine, следуя руководству AngularJs unit testing. Тесты выполняются с помощью Karma runner.

Проблема заключается в том, что они не могут выполняться одновременно из-за утечек памяти. Во время их запуска память накапливается независимо от того, в каком браузере они запускаются, и в какой-то момент браузер выходит из строя и отключается. Лучшее обходное решение, о котором я сейчас знаю, которое используется в сообществе, имеющем эту проблему, - это сплит тестов в нескольких прогонах и, в конце концов, получает правильный охват, объединяя результаты с одиночными прогонами.

Когда я впервые встретился с этой проблемой, у меня было около 1000 тестов. После попытки использования всех доступных браузеров для работы я разделил тесты на несколько прогонов, однако оказалось, что это не очень хорошо обходится в течение длительного времени. Теперь тесты выполняются в 14+ одиночных запусках, которые запускаются параллельно, чтобы сократить время завершения и все еще IMO, это не может надолго решить проблему, но задержать ее бит litle из-за ограничения ресурсов (ОЗУ, ЦП) и раздражающего потребления времени.

Кто-то может утверждать, что у меня есть утечки памяти в моем коде, для которых я не могу гарантировать, даже если у меня нет никаких проблем при запуске приложения в браузере. Вот почему я создал пример проекта, который выделит эту проблему.

Там для воспроизведения этой проблемы я создаю Angular service, который тяжел в потреблении памяти следующим образом:

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

После этого у меня есть простой directive, который использует эту службу для инициализации многих элементов 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());
    }
  };
});

И в конце я динамически регистрирую 1000 тестовых наборов с определение теста для директивы, которую btw пишет, как предложено в Angular руководство по тестированию устройств.

// define multiple suits with the same definition just for showcase
for (var i = 0; i < 1000; i += 1) {
  describe('heavyLoad directive #' + i, testDefinition);
}

Чтобы попробовать пример, просто проверьте проект с GitHub и перед запуском запуска кармы:

$ npm install
$ bower install

Я с нетерпением жду, где найти проблему и решить ее наконец.

Приветствия

Ответ 1

Проблема заключалась в забытой очистке, которая должна быть выполнена после каждого теста. После его добавления число тестов больше не имеет значения, потому что потребление памяти стабильно и тесты можно запускать в любом браузере.

Я добавил модификацию предыдущего определения теста здесь, в котором показано решение с успешным выполнением 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();
    });

});

Есть две вещи, которые необходимо очистить:

  • скомпилированный элемент при использовании $compile для директив тестирования
  • все переменные в области описания функций

Два из них сложны и трудно найти и принять во внимание. Для первого я уже знал, но это мало помогло, пока я не обнаружил второе, связанное с тем, как работает Жасмин внутри. Я создал issue в своем репозитории GitHub, который должен помочь найти лучшее решение или, по крайней мере, распространить эту информацию среди разработчиков быстрее.

Я надеюсь, что этот ответ будет полезен для многих людей, имеющих эту проблему. Я также напишу некоторую информацию после того, как закончу рефакторинг всех моих других тестов.

Ура!