AngularJs unit testing memory leaks

Jak być może już wiesz, wielu z nas, którzy mają dużą ilość pisemnego testu jednostkowego, spotkało się z tym nie trywialnie rozwiązywalnym problemem. Mam około 3500 + testów jednostkowych napisanych w składni Jasmine zgodnie z przewodnikiem AngularJs unit testing . Testy wykonywane są za pomocą karmy runner.

Problem polega na tym, że nie mogą być wykonane wszystkie na raz z powodu wycieków pamięci. Podczas ich uruchamiania pamięć gromadzi się bez względu na to, w jakiej przeglądarce są uruchamiane i w pewnym momencie przeglądarka zawiesza się i rozłącza. Najlepszym obejściem, o którym wiem, które jest obecnie używane w społeczności, która ma ten problem, jest dzielenie testów na wiele przebiegów, a na koniec uzyskanie prawidłowego pokrycia poprzez połączenie wyników z pojedynczych przebiegów.

Kiedy po raz pierwszy spotkałem się z tym problemem miałem około 1000 testów. Po wypróbowaniu wszystkich dostępnych przeglądarek do uruchomienia podzieliłem testy na kilka uruchomień, jednak okazało się, że jest to niezbyt dobre obejście przez długi czas. Teraz testy są wykonywane w 14 + pojedynczych uruchomieniach, które są uruchamiane równolegle, aby skrócić czas kompletacji i nadal IMO nie może to trwale rozwiązać problemu, ale opóźnić go trochę ze względu na ograniczenie zasobów (RAM, CPU) i irytujące zużycie czasu.

Ktoś może argumentować, że mam wycieki pamięci w kodzie, których nie mogę zagwarantować, mimo że nie mam żadnych problemów z uruchomieniem aplikacji w przeglądarce. Dlatego Stworzyłem przykładowy projekt, który uwypukli ten problem.

Tam dla odtworzenia tego problemu tworzę Angular service który jest ciężki w zużyciu pamięci Tak:

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

Po tym mam prostą dyrektywę która używa tej usługi do inicjalizacji wielu elementów 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());
    }
  };
});

I na koniec dynamicznie rejestruję 1000 zestawów testowych z definicją testu dla dyrektywy, która BTW jest zapisana jako sugerowane w Przewodniku Angular unit testing.

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

Aby wypróbować przykład wystarczy sprawdzić projekt z GitHub i przed uruchomieniem Karma start run:

$ npm install
$ bower install

Z niecierpliwością czekam na znalezienie problemu i rozwiązanie go w końcu.

Cheers

Author: S.Klechkovski, 2015-10-07

1 answers

Problem tkwił w zapomnianym sprzątaniu, które musi być wykonane po każdym teście. Po jego dodaniu liczba testów nie ma już znaczenia, ponieważ zużycie pamięci jest stabilne i testy można uruchomić w dowolnej przeglądarce.

Dodałem modyfikację poprzedniej definicji testu tutaj, która pokazuje rozwiązanie z pomyślnym wykonaniem 3000 testów zarejestrowanych dinamicznie.

Oto jak teraz wygląda test:

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

});

Są dwie rzeczy, które trzeba posprzątać:

  • compiled element podczas używania $compile do testowania dyrektyw
  • wszystkie zmienne z zakresu opisywania funkcji

Te dwa są trudne i trudne do znalezienia i wzięcia pod uwagę. Dla pierwszego już wiedziałem, ale to nie pomogło, dopóki nie odkryłem drugi, który jest związany z tym, jak Jasmine działa wewnątrz. Stworzyłem problem w repozytorium GitHub, który powinien pomóc w znalezieniu lepszego rozwiązanie lub przynajmniej rozpowszechniać te informacje wśród deweloperów szybciej.

Mam nadzieję, że ta odpowiedź będzie pomocna dla wielu osób mających ten problem. Napiszę też kilka informacji po zakończeniu refaktoryzacji wszystkich moich innych testów.

Zdrówko!
 24
Author: S.Klechkovski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-16 09:50:41