Angular w praktyce

Szybsze testy w Angular dzięki ng-bullet

Angular i testy? W tym artykule pokażę w jaki sposób poprawisz wydajność przez wykorzystanie ng-bullet oraz jak ng-bullet wypada na tle alternatywnych rozwiązań, jak karma-parallel i jest.
Szybsze testy w Angular dzięki ng-bullet

Pisanie testów w Angularze w dużych projektach jest problematyczne ze względu na problemy z wydajnością. W tym artykule pokażę narzędzie, które możesz wykorzystać w swoim projekcie, aby przyśpieszyć działanie testów. Tym narzędziem jest ng-bullet. Zgodnie z opisem przygotowanym przez autora biblioteki Nikita Yakovenko @topnotch48:

Bullet is a library which enhances your unit testing experience with Angular TestBed, greatly increasing execution speed of your tests.

Warto podkreślić, że ng-bullet korzysta z domyślnego dla Angulara zestawu jasmine + karma, przez co przesiadka jest relatywnie prosta i wymaga zaledwie paru zmian w kodzie.

W dalszej części artykułu pokażę ci w jaki sposób korzystać z ng-bullet, jakie są alternatywy i jak wprowadzenie Ivy zmienia sposób pisania testów w Angularze.

Zaczynamy!

🐢 Dlaczego testy w Angular są takie wolne?

Powodów, dla których testy w Angular są powolne jest kilka, ale najczęściej komentowanym jest automatyczne wywoływanie przez Angular TestBed.resetTestingModule przed każdym testem w Jasmine beforeEach. Powoduje to, że komponenty i moduły są kompilowane niepotrzebnie za każdym razem a co za tym idzie czas trwania testów wydłuża się niemiłosiernie. Więcej o tym i innych problemach przeczytasz w otwartym od 2016 roku TestBed.configureTestingModule Performance Issue.

Rozwiązanie?

Wydawać by się mogło, że nic prostszego, niż przenieść wywołanie createTestingModule z beforeEach do beforeAll, tak aby kompilacja komponentów odbywała się raz dla wszystkich testów. Okazuje się, że nie jest to takie proste.

Po pierwsze takie rozwiązanie zakłada, że będziesz zmuszony grupować w jeden blok describe wszystkie testy, które mogą używać takiej samej konfiguracji TestBed. Te, które nie pasują do tego schematu, będą musiały być przeniesione w inne miejsce.

Po drugie może to prowadzić do błędów, które wcześniej w tym samym teście się nie pojawiały. Przez to będziesz zmuszony przepisać sporą część kodu testowego. Spodziewaj się częstego występowania znanego Cannot configure the test module when the test module has already been instantiated 😆.

Można oczywiście pójść w drugą skrajność i umieścić wszystkie testy w ramach jednego wywołania it, przez co faktycznie kompilacja modułów i komponentów nastąpi tylko raz, ale to rozwiązanie z granicy absurdu, którym nie warto się zajmować.

A co na to Angular Team?

Rozwiązaniem mogłaby być drobna zmiana w kodzie umożliwiająca odłączenie TestBed.resetTestingModule za pomocą flagi, lub programistycznie. Mogłoby to dać programistom możliwość kontrolowania kiedy ten mechanizm jest potrzebny, a kiedy nie. W efekcie takie rozwiązanie pozwoliłoby przyspieszyć znaczną część testów, szczególnie w dużych projektach. Przykładowa poprawka jest pokazana w

https://github.com/angular/angular/pull/17710

Jednak zespół Angulara nie kwapi się, żeby ułatwić życie programistom. Jest to związane z planami dotyczącymi Ivy, które za pomocą wewnętrznego cache rozwiązuje problem wielokrotnej kompilacji modułów i komponentów.

However in Ivy TestBed the logic was updated to avoid unnecessary recompilations between test runs, so you should see performance improvement after switching to Ivy.

źródło

Więcej o Ivy przeczytasz w dalszej części artykułu.

📜 Przykłady użycia

Biorąc powyższe pod uwagę, w jaki sposób możesz przyspieszyć działanie swoich testów za pomocą ng-bullet? Standardowo, wszystkie tutoriale i kursy Angulara pokazują następujący sposób na tworzenie konfiguracji testowej aplikacji. Jest on w pełni poprawny, jednak posiada mankament, o którym pisałem w poprzedniej sekcji — każdy element TestBed będzie kompilowany przed każdym uruchomionym testem, co znacząco zwiększa czas wykonania testów.

  beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [ /*list of components goes here*/ ],
            imports: [ /* list of providers goes here*/ ]
        })
        .compileComponents();
  }));

O ile problem ten nie jest znaczący w małych projektach, może dać się we znaki w projektach w których liczba testów idzie w setki, jeśli nie w tysiące.

Aby przyspieszyć działanie testów, możesz skorzystać z ng-bullet.

  import { configureTestSuite } from 'ng-bullet';
    ...
    configureTestSuite(() => {
        TestBed.configureTestingModule({
            declarations: [ /*list of components goes here*/ ],
            imports: [ /* list of providers goes here*/ ]
        })
    });

Zmiany w porównaniu do poprzedniej wersji są małe, ale znaczące.

  1. Zamiast beforeEach, korzystamy z configureTestSuite.
  2. Nie wołamy compileComponents – to ng-bullet będzie dbał o to, żeby komponenty zostały skompilowane tylko jeden raz.
  3. Instancje komponentów i serwisów będą tworzone przed każdym testem, aby zapewnić izolację.

Niewiele prawda? Zobaczmy zatem co daje nam tak stworzona konfiguracja.

🚀 Korzyści

Korzyści wynikające z zastosowania ng-bullet rosną w większych projektach. Im więcej testów integracyjnych, dotykających bezpośrednio działania komponentów, tym większy wpływ będzie miał ng-bullet na całkowity czas wykonania testów.

Przykładowo, w prawdziwym projekcie komercyjnym, nad którym obecnie pracujemy, większość testów to małe i szybkie testy jednostkowe, weryfikujące logikę biznesową zawartą w serwisach. Dodatkowo sporą grupę stanowią testy integracyjne korzystające z TestBed. W tym projekcie, przed wprowadzeniem ng-bullet pełen zestaw +500 testów odpalał się w czasie ponad 5 minut i 20 sekund.

Angular test without ng-bullet
Przed wprowadzeniem ng-bullet

Po przerobieniu testów zgodnie z opisem, testy wyraźnie przyspieszyły i teraz odpalają się w czasie 2 minut i 30 sekund, a więc ponad dwukrotnie szybciej.

Angular test with ng-bullet
Po wprowadzeniu ng-bullet

Biorąc pod uwagę specyfikę projektu, jego rozmiar i stopień złożoności komponentów, takie przyspieszenie jest dobrym wynikiem. Oczywiście, mogłoby być lepiej, ale biorąc pod uwagę zainwestowany czas na dodanie biblioteki i przerobienie kodu – jakieś 20 minut – inwestycja szybko zacznie się zwracać.

A jak wypada ng-bullet na tle innych rozwiązań?

🔴 Alternatywy

Oprócz wykorzystania ng-bullet mamy do dyspozycji parę innych rozwiązań, sprawdzonych w dużych projektach, z których warto wymienić karma-parallel oraz jest.

karma-parallel

Innym podejściem pozwalającym przyśpieszyć uruchamianie testów jest uruchomienie ich równolegle za pomocą pluginu karma-parallel. Nie jestem wielkim fanem tego podejścia – nie sprawdziło się zbyt dobrze w projektach, w których do tej pory pracowałem. Idea działania karma-parallel jest dosyć prosta. Polega na uruchomieniu jednocześnie wielu instancji przeglądarki – niezależnie czy jest to zwykły Chome, czy Chrome Headless – i rozdzieleniu testów pomiędzy instancje.

Pomysł jest dobry, natomiast problematyczna może okazać się konfiguracja. O ile samo zebranie wyników testów jest raczej proste przy uruchomieniu równoległym, to policzeniu coverage w takim przypadku, stanowi dla karmy i jej pluginów dużo większe wyzwanie. Za każdym razem, gdy korzystałem z karma-parallel pojawiały się te same problemy. Karma gubiła coverage…

jest

Biblioteka jest stanowi zupełnie odmienne podejście do uruchamiania testów. Napisana w Facebook, jest zwykle stosowana do aplikacji w reactJs, jednak integracja z Angular jest również bezbolesna.

jest zastępuje domyślnie działającą z Angularem karme w roli test runnera. W przeciwieństwie do karmy korzysta z jsdom zamiast rzeczywistej przeglądarki. Jest zbudowany na bazie jasmine, dzięki czemu posiada zbliżony interfejs, co znacznie ułatwia przesiadkę. Powstał nawet gotowy szablon jest-schematic, który zintegruje istniejący projekt Angular z jest.

Bez dwóch zdań jest jest świetną biblioteką i warto zainwestować czas, żeby ją przetestować. Tym bardziej, że inwestycja ta może się szybko zwrócić. Przykładowo @Nrwl raportuje czterokrotną poprawę szybkości działania testów w Nx 6.3: Faster Testing with Jest

🎉 Angular 9 i Ivy

Innego rodzaju alternatywą do ng-bullet jest przesiadka na Angular 9. Gdy Ivy ujrzało światło dzienne, sytuacja z TestBed uległa znacznej poprawie. Wygląda na to, że implementacja TestBed została poprawiona i komponenty nie są już kompilowane przed każdym testem. Nasze testy na projektach komercyjnych, w których używamy Angular 9 potwierdzają znaczne przyspieszenie, nawet bez użycie ng-bullet.

Gdy korzystasz z Ivy, domyślnie jako implementacja TestBed będzie wykorzystywany TestBedRender3. To nic innego jak TestBed dla Render 3. Ivy posiada wbudowany mechanizm cache do przechowywania skompilowanych modułów i komponentów. Dzięki temu wywołanie TestBed.configureTestingModule(...).compileComponents() będzie szybsze. Kompilacja nastąpi tylko przy pierwszym wywołaniu a pozostałe zostaną pominięte.

⚡ Podsumowanie

Biblioteka ng-bullet jest świetnym sposobem na zwiększenie wydajności twoich testów – co do tego nie ma żadnych wątpliwości. Jest szczególnie przydatna w dużych projektach, gdzie liczba testów idzie w tysiące. ng-bullet ogranicza kompilację modułów i komponentów do minimum poprzez przesunięcie wywołania TestBed.resetTestingModule z beforeEach do beforeAll.

Podobne korzyści w szybkości wykonywania testów można osiągnąć z Ivy. Zostało w nim zastosowane inne podejście polegające na stworzeniu cache na skompilowane komponenty nie zmieniając jednocześnie sposobu wykonywania testów.

Jeżeli pracujesz z projektami Angular 9, korzyści z ng-bullet będą mniej odczuwalne. Jeśli jednak pracujesz na starszych projektach – śmiało, spróbuj ng-bullet. Ta biblioteka jest właśnie dla ciebie 🅰️.

Podziel się artykułem

Share on facebook
Share on twitter
Share on linkedin
Sławek Plamowski

Na co dzień zajmuje się tematami z zakresu architektury aplikacji biznesowych, zwinnego wytwarzania oprogramowania oraz continuous delivery. W pracy programisty najbardziej pasjonuje go rozwiązywanie problemów dotyczących procesu wytwarzania oprogramowania jako całości i budowanie samoorganizujących się zespołów. Wyznaje pogląd, że bycie wykwalifikowanym programistą to za mało, a umiejętności komunikacji w zespole i produktywnego planowania są w dzisiejszym świecie niezbędne do osiągnięcia swoich celów.

Sławek Plamowski

Lead Software Developer