Migracja testów automatycznych do Azure Pipelines
11.07.2022 | Mateusz Czerwiński
Wstęp
Czy rozwiązania chmurowe w połączeniu z zamkniętymi środowiskami testowymi nadają się do testów automatycznych? Jak sobie z tym wszystkim poradzić? Na przykładzie aplikacji MojeING opowiem Wam z jakimi wyzwaniami musieliśmy się zmagać, podczas automatyzacji testów w Azure Devops. Zapraszam do krótkiej relacji.
Jak to było kiedyś?
Testy automatyczne w projekcie MojeING rozwijaliśmy używając lokalnych rozwiązań takich jak, Jenkins oraz Gitlab.
Jenkins służył nam jako główny wyzwalacz zadań (nocnych) przygotowujący raporty z uruchamianych testów automatycznych, tak aby były gotowe na początek każdego dnia pracy. Taki raport pomagał testerowi przy porannej kawie zapoznać się ze stanem aplikacji po zmianach wykonanych przez developerów poprzedniego dnia.
Gitlab zaś był naszą osobistą ambicją, aby wynieść procesy Continous Integration (CI) na wyższy poziom. Naszym celem było, aby poprzez testy automatyczne integrować pracę inżynierów testów z developerami. Cel ten osiągnęliśmy przy pomocy Gitlaba i Gitlabrunnera wykorzystując potencjał tych narzędzi w zakresie uruchamiania, nie tylko testów jednostkowych czy też testów sprawdzających standard kodowania (lint testy), ale również funkcjonalne testy automatyczne oparte o framework Nightwatch.js. Więcej o tym projekcie możecie również przeczytać w moim poprzednim artykule.
Przez wiele miesięcy w procesie Merge request wspieraliśmy się uruchamianymi paczkami testów przez co skutecznie kontrolowaliśmy kod wchodzący do głównych gałęzi projektu. W przypadkach, gdy testy automatyczne znajdywały błędy możliwość dodania kodu do gałęzi była blokowana i wymagała podjęcia działań przez developera lub testera.
Oczywiście napotykaliśmy na szereg utrudnień, którym testerzy dzielnie stawiali czoła każdego dnia. Głównym problemem było to, że wspomniany Gitlabrunner – czyli aplikacja odpowiedzialna za komunikację z Gitlabem i obsługę testów automatycznych była instalowana na maszynach wirtualnych (VDI). Wszystko w naszej wewnętrznej infrastrukturze bankowej. Częste aktualizacje systemowe, restarty tych maszyn, zmieniające się konfiguracje itp. mocno wpływały na stabilność naszych testów. To wszystko powodowało, że tester miał sporo dodatkowej pracy.
Idzie nowe - lepsze!
Zgodnie z powszechnie znanym powiedzeniem, że „jedynym pewnym jest zmiana”, na wiadomość o Azure od razu zabraliśmy się do pracy.
Zaczęło się od szeregu spotkań i prezentacji pokazujących nam możliwości i wielkość rozwiązania Azure. Potwierdziliśmy sobie pewnie „oczywistości” - że chmura to przyszłość, większe bezpieczeństwo, lepsza dostępność, możliwości rozwoju, łatwe skalowanie środowisk i nowoczesność – a potem zaczęliśmy konkretne prace nad przenoszeniem repozytorium kodu.
Rzecz jasna, musieliśmy zacząć od repozytorium, gdyż bez tego nie można rozpocząć prac z testami. Tego etapu nie będę szczegółowo opisywał, ponieważ nie jest to istotne z punktu widzenia automatyzacji testów. W każdym razie, przebrnęliśmy przez szereg zadań związanych z uprawnieniami, konfiguracją itp. tak aby w końcu mieć działające repozytorium kodu projektu MojeING, które otwierało nam drogę do dalszych działań.
Pierwsze problemy - czyli wyzwania
Wczytując się w dokumentację oraz konsultując się ze specjalistami odpowiedzialnymi za wdrożenie, ustalaliśmy krok po kroku co potrzebujemy zrobić aby móc uruchomić testy automatyczne.
Azure Pipeline miał służyć jako główny wyzwalacz testów. W swoich założeniach Azure uruchamia pipeline na dynamicznie powoływanych maszynach wirtualnych - tzw. workspace. Czyli w skrócie, na takiej maszynie powołanej na czas działania pipeline pobierany jest kod źródłowy projektu z Azure Repos, potem zwykle pobierane są dodatkowo potrzebne zależności z Azure Artifacts. Następnie przy użyciu składni YAML wszystko to jest układane w odpowiednie sekwencje i następuje wykonywanie zadań według harmonogramu.
Brzmi prosto i przyjemnie? Niestety, pojawił się pewien problem. Azure jest w chmurze „w internecie”, a nasze środowiska testowe, na których zamierzaliśmy uruchomić nasze testy, są w zamkniętym i zabezpieczonym środowisku bankowym. Okazało się to dla nas sporym wyzwaniem i spowodowało konieczność opracowania standardu, który umożliwiałby testowanie automatyczne używając CI/CD w chmurze Azure.
Powrót do VDI
Z racji tego, że nie mieliśmy możliwości połączenia do bankowych środowisk testowych bezpośrednio z maszyn Azure (tych które nazwałem ‘workspace’ dla projektu), to nie mogliśmy korzystać z tych maszyn do uruchomienia testów. W toku prac wybraliśmy Ansible, który daje możliwość nawiązywania połączenia zdalnego ze wskazaną maszyną. Idąc dalej dotarliśmy do dobrze nam znanego, wcześniejszego rozwiązania czyli maszyn VDI.
I tak zatoczyliśmy koło, ponieważ korzystając z puli Agentów Azure, łączymy się w naszych pipeline z maszynami VDI, na których uruchamiamy testy. I wszystko działa, bo VDI mają dostęp do potrzebnych środowisk testowych.
Zanim przejdę do szczegółów technicznych i przykładów, warto podsumować, że zamieniliśmy lokalnego Jenkinsa i Gitlaba na chmurowego Azure.
Wraz z Azure i dostępnym w nim Ansible, zyskaliśmy też nowe możliwości zarządzania konfiguracją, co było dla nas niezwykle ważne z punktu widzenia stabilności VDI. Jest to nowy kierunek rozwoju naszych inżynierów i sporo nowych wyzwań, których z chęcią się podejmują.
Coś zaczyna działać - czyli pierwsze wypracowane koncepcje
Ostateczne rozwiązanie oparliśmy o trzy poziomy, czy też etapy. Po pierwsze uruchamiamy pipeline w Azure Devops według przyjętego harmonogramu. Wewnątrz pipeline korzystamy z Ansible i łączymy się do wewnętrznej infrastruktury, a na koniec już bezpośrednio na VDI testujemy aplikacje.
Czyli po kolei:
- AzureDevops
- Kod testów automatycznych musi być w repozytorium kodu (Azure Repos)
- Wszystkie zależności (np. npm, mvn, gradle itd) potrzebne do działania projektu muszą znaleźć się w Azure Artifacts
- Pipeline i skrypty Ansible
- W Pipeline definiujemy potrzebne kroki do realizacji
- W Ansible playbook zdefiniowane są konkretne zadania uruchamiane na VDI
- W pliku YAML definiuje się listę VDI na jakich mają wykonywać się testy automatyczne
- VDI
- Instalacja stosu technologicznego do uruchomienia testów oraz aplikacji (np.git, nodejs, npm, maven, gradle, java itd)
- Instalacja przeglądarek (Chrome, Firefox, Edge)
- Konfiguracja umożliwiająca wykonywanie zdalnych poleceń
Rysunek 1. Schemat rozwiązania
Źródło: opracowanie własne na podstawie materiałów wewnętrznych ING Banku Śląskiego.
Założenia przy tworzeniu pipeline
Pipeline może być realizowany na wiele sposobów. Pomysłów i rozwiązań może być tyle, ile osób mierzących się z takim zadaniem. W oparciu o przedstawioną powyżej architekturę, uzgodniliśmy ramy, założenia dla tworzenia pipeline. Rozwiązanie takie jest bardzo pomocne dla osób dołączających do projektu, pomaga im stawiać pierwsze kroki przy pisaniu pipeline.
Poniżej lista założeń jakie przyjęliśmy:
- Dany pipeline wykonuje się na maszynie VDI
- Pipeline składa się z dwóch 'stage': budowanie aplikacji na VDI oraz uruchomienie testów automatycznych na VDI
- Pipeline składa się z X ilości zadań (jobów), (przy czym 1 zadanie w pipeline to 1 zadanie w dawnym Jenkins)
- Uruchomienie etapu 'stage: Tests' zależne jest od powodzenia etapu 'stage: Build' czyli: dependsOn: Build
- Uruchamianie kolejnych zadań z testami w ramach etapu 'stage: tests' zależne jest od zakończenia poprzedniego zadania (jest to związane z tym, że standardowo zadania uruchamiają się równolegle. Przy ich większej ilości może to być zbyt dużym obciążeniem wydajności VDI) czyli: dependsOn: name_previous_job
- Każde zadanie posiada parametr continueOnError: true dzięki czemu poszczególne zadania z testami wykonują się nawet jeśli poprzednia paczka testów zawierała błędy
- Na koniec każdego zadania wysyłany jest raport z testów, dzięki czemu na bieżąco po każdej zakończonej paczce wyniki trafiają do zainteresowanych.
- W pipeline używamy tzw. - template:, które pozwalają wydzielić fragmenty skryptów tak aby móc ich użyć wielokrotnie
- W pipeline używamy tzw. parameters:, które w połączeniu z - template: pozwalają dynamicznie zmieniać parametry podczas uruchamiania
Ostateczną wersję proponowanego pipeline przedstawiam na schemacie poniżej. Oczywiście aktualne wersje mogą się różnić w zależności od potrzeb projektowych.
Rysunek 2. Ostateczna wersja proponowanego pipeline
Źródło: opracowanie własne na podstawie materiałów wewnętrznych ING Banku Śląskiego.
Sposób działania takiego pipeline przedstawia się następująco:
- W pierwszej kolejności musimy zbudować na VDI aplikację w oparciu o dany branch, w tym celu w ramach etapu ‘Build’ korzystamy z playbooka w którym opisujemy kroki:
- pobieranie kodu z repozytorium,
- pobieranie zależności,
- budowanie aplikacji
- wysłanie raportu z wynikiem budowania aplikacji
- Gdy budowanie aplikacji zakończy się sukcesem można przystąpić do uruchamiania testów, w tym celu w ramach kolejnych etapów (ich ilość zależy tylko od naszych potrzeb) wykonujemy kroki:
- Uruchomienie paczki testów
- Wysłanie raportu z wynikami
- Uruchomienie kolejnej paczki testów
- Itd.
- Na koniec można jeszcze wykonać czynności mające na celu „posprzątanie” po wykonywanych testach. Mam na myśli sprawdzenie np. czy jakieś drivery nie pozostały otwarte, bądź czy nie trzeba zamknąć otwartych sesji przeglądarek.
Mamy to!
Finalnie wdrożyliśmy rozwiązanie zgodnie z planem. Zdążyliśmy z opracowaniem koncepcji, stworzeniem dokumentacji i podzieleniem się wiedzą. Przebyta droga była kręta i wyboista, ale efekt końcowy na tyle satysfakcjonujący, że obecnie możemy z uśmiechem na ustach wspominać niektóre „sytuacje”.
Kiedy nauczyliśmy się już sprawnie czerpać korzyści z nowego rozwiązania, przyzwyczailiśmy się do nowej rzeczywistości, zaczęliśmy myśleć o rzeczach których nie zrobiliśmy. Postanowiliśmy się zmierzyć z czymś, co wcześniej wydawało się niemożliwe, podnieść poprzeczkę i podjąć się nowych wyzwań. Niech to będzie zapowiedź tego co miało nadejść...
A może by tak tylko w chmurze?
Największym niedosytem jaki czuliśmy było korzystanie z VDI w wewnętrznej infrastrukturze. Od tego momentu nasze prace skupiały się na wymyśleniu jak zoptymalizować pipeline, aby najlepiej wykorzystać potencjał Azure, nie wychodzić poza jego infrastrukturę i dostarczyć developerom informację zwrotną możliwie najszybciej jak się tylko da.
Po ponownej analizie dopasowaliśmy do siebie kilka elementów:
- Projekt aplikacji może działać w całkowitej izolacji od zintegrowanych środowisk
- Do dyspozycji mieliśmy maszyny wirtualne Azure
- Testy automatyczne mamy w tej samej technologii, co projekt aplikacji MojeING
- Zasoby maszyn wirtualnych Azure dynamicznie dostosowują się do naszych potrzeb
Te cztery aspekty spowodowały, że byliśmy w stanie stworzyć pipeline, który w oparciu o zbudowaną wersję aplikacji mógł uruchamiać testy automatyczne bezpośrednio na maszynach wirtualnych Azure. Bez konieczności łączenia się i korzystania z wewnętrznej infrastruktury VDI.
I w tym momencie dysponowaliśmy podobnymi możliwościami jak korzystając z Gitlab i Gitlabrunner. Mogliśmy ponownie rozszerzyć kontrolę testów automatycznych i funkcjonalnych na etapie ‘Pull request’ tak, aby do głównej gałęzi kodu nie trafiały błędy, które wychwycimy na etapie testów.
Rysunek 3. Schemat pipeline dla PullRequest
Źródło: opracowanie własne na podstawie materiałów wewnętrznych ING Banku Śląskiego
Taki pipeline w skrócie, działa tak:
- Pobiera aktualną wersję projektu z repozytorium na danym branchu
- Uruchamia równolegle 3 zadania: budowanie frontu aplikacji, testy jednostkowe oraz testy standardu kodowania
- Następnie zadanie budujące aplikację publikuje artefakt oraz wszystkie zależności w postaci spakowanej w formacie .zip
- Jeśli zadanie z budowaniem aplikacji zakończy się sukcesem, uruchamiany jest drugi etap z testami funkcjonalnymi
- Na etapie testów funkcjonalnych zadania z poszczególnymi paczkami testów uruchamiają się równolegle tak aby maksymalnie skrócić czas wykonywania się testów
- Każde z zadań pobiera artefakt z aplikacją, rozpakowuje ją i uruchamia testy
- Przygotowane scenariusze testów wykonują się bezpośrednio na uruchomionej aplikacji
- I na koniec jeśli wszystkie testy zakończą się z wynikiem pozytywnym dany pullRequest będzie możliwy do mergowania do głównej gałęzi kodu
Dzięki tak przygotowanemu pipeline możemy efektywniej korzystać z możliwości i potencjału Azure. W razie potrzeb możemy dokładać scenariuszy testowych, dzielić zadania i delegować je na kolejne maszyny dynamicznie powoływane w ramach dostępnych zasobów.
I na koniec...
Można odnieść wrażenie, że wiele przeszliśmy i znaleźliśmy ostateczne rozwiązanie. Nic bardziej mylnego. Znaleźliśmy rozwiązanie optymalne i pasujące na dany moment, które było możliwe do realizacji w zadanych warunkach. Przed nami jednak kolejne wyzwania i możliwości optymalizacji. Zespoły podczas wdrożenia zdobyły cenne doświadczenia i wiedzę z zakresu obsługi i możliwości narzędzia Azure. Sytuacja i oczekiwania ciągle się zmieniają, co będzie generowało coraz to nowsze i lepsze pomysły jak wykorzystać potencjał Azure.
Chciałem na koniec gorąco podziękować wszystkim zaangażowanym w prace nad tym standardem - mimo wielu przeszkód udało się zrealizować z sukcesem zamierzone cele. Wszystkim tym, którzy po przeczytaniu tego artykułu zauważają elementy, które można poprawić bądź rozwinąć jakieś pomysły gorąco zachęcam do kontaktu ze mną i zapraszam do współpracy.