Poziomy Zaufania: ramy jakości w CI/CD
Nota od autora: Poniższy artykuł jest zapisem koncepcji, którą przedstawiłem podczas konferencji Agile & Automation Days 2015 w Krakowie pod tytułem “Permanentna Integracja”. Od czasu tego wystąpienia wiele się zmieniło, w tym podejście do modelowania repozytoriów kodu (odchodzenie od GitFlow na rzecz trunk-based development), ale problemy opisane w prezentacji pozostają te same.
Poziomy zaufania do oprogramowania (Software Trust Levels)
Streszczenie
Proces Ciągłej Integracji (CI) został w ostatnim czasie szeroko zaadaptowany w różnych branżach wytwarzania oprogramowania. W teorii przynosi on szereg korzyści, takich jak natychmiastowa informacja o stanie produktu i redukcja kosztów projektu. W praktyce jednak systemy CI często ewoluują do stanu, w którym wszystkie korzyści są niwelowane przez wysoki poziom skomplikowania, błędne założenia i niewłaściwy projekt systemu testowego. Nieprawidłowo zaprojektowane i skalowane systemy CI generują dodatkowe koszty utrzymania, nie dając wyraźnych korzyści.
W tym artykule autor przedstawi praktyczne wskazówki, jak utrzymać jakość produktu oprogramowania w dobrej kondycji przy pomocy dobrze zaprojektowanego procesu Ciągłej Integracji. Celem artykułu jest zaproponowanie procesu wytwórczego, struktury repozytorium kodu oraz metryk jakości. Kompozycja tych elementów może być kluczem do utrzymania jakości oprogramowania przy dynamicznie zmieniających się wymaganiach.
Tło
Proces produkcji oprogramowania ma wiele analogii do procesów produkcyjnych w innych branżach. Aby uzyskać produkt wysokiej jakości, konieczne jest posiadanie bardzo dobrze zorganizowanego warsztatu produkcyjnego. Dodatkowo, produkty wysokiej jakości wymagają specjalistycznych narzędzi. Implikuje to potrzebę posiadania zaawansowanej wiedzy na temat obsługi tych narzędzi. Produkty wysokiej jakości muszą być testowane poprawnie i bardzo dokładnie. Wyniki testów powinny być wykorzystywane do ciągłego doskonalenia procesu produkcji. Na końcu produkt musi zostać odpowiednio zapakowany i dostarczony do klienta.
Powyższa analogia oddaje specyfikę produkcji oprogramowania. Ostatnie trendy w branży kładą ogromny nacisk na automatyzację procesu tworzenia, testowania i dostarczania oprogramowania. Istnieje wiele narzędzi programistycznych zaprojektowanych do wspierania procesów CI. Jednak, aby wykorzystać te narzędzia w sposób optymalny, organizacje muszą zdefiniować zasady procesu CI, takie jak ogólna struktura repozytorium kodu i testów, zasady oceny jakości kodu oraz reguły automatyzacji i wykonywania testów.
Z obserwacji wynika, że poziom chaosu w projekcie był zawsze odwrotnie proporcjonalny do poziomu zrozumienia procesu dostarczania kodu między gałęziami repozytorium (w tym dostarczania na środowisko produkcyjne). Zespoły deweloperskie potrzebują jasnych zasad pracy. Koncepcja Poziomów Zaufania do Oprogramowania pomaga natychmiast ocenić jakość produktu. Poziom zaufania jest przypisywany produktowi na podstawie wyników różnych punktów walidacji (wynik punktu walidacji powinien być zawsze binarny – może przyjmować wartość prawda lub fałsz).
Z założenia koncepcja ta jest skalowalna, ponieważ zawartość zbioru z punktami walidacji jest elastyczna i powinna być definiowana odpowiednio przez organizację. Proponowane ramy poziomów zaufania mają być łatwe do zrozumienia i łatwe do skalowania.
Zasady testowania w modelach Ciągłej Integracji
Według ISTQB, istnieje siedem zasad testowania oprogramowania, które są zgodne z naszą koncepcją Poziomów Zaufania:
- Testowanie ujawnia obecność usterek: Testowanie nie może być traktowane jako dowód na to, że oprogramowanie jest wolne od błędów. Ograniczamy liczbę nieodkrytych defektów, ale nigdy nie możemy powiedzieć, że produkt jest idealny.
- Testowanie gruntowne jest niemożliwe: W praktyce nie możemy sprawdzić wszystkich przypadków użycia. Ważne jest, aby być świadomym ryzyka i wybrać odpowiedni podzbiór scenariuszy do testowania. Koncepcja poziomów zaufania pomaga wybrać właściwy podzbiór w celu mitygacji ryzyka.
- Wczesne testowanie: Rozpoczęcie testowania tak wcześnie, jak to możliwe, pomaga znaleźć krytyczne błędy. Rozszerzenie tej zasady przedstawiono w definicji poziomu zaufania.
- Kumulowanie się błędów: Zazwyczaj większość błędów jest związana z ograniczonym zestawem funkcji oprogramowania. Identyfikacja tego zestawu jest najskuteczniejszą metodą testowania.
- Paradoks pestycydów: Powtarzanie w kółko tych samych przypadków testowych nie pomaga w znajdowaniu nowych błędów. Zaleca się przegląd scenariuszy, dodawanie nowych i przenoszenie innych na inny poziom zaufania.
- Testowanie zależy od kontekstu: Różne moduły w produkcie mogą mieć zdefiniowane różne punkty walidacji, aby osiągnąć ten sam poziom zaufania.
- Błąd założenia o braku błędów: Dobrze przetestowane oprogramowanie, które pomija kluczowe wymagania, jest bezużyteczne.
Najprostszym sposobem na spełnienie powyższych zasad jest automatyzacja wykonywania przypadków testowych i orzekania o wyniku. Kolejnym poziomem dojrzałości produkcji oprogramowania jest adaptacja koncepcji Ciągłej Integracji (Continuous Integration), zdefiniowanej następująco:
“Ciągła Integracja (CI) to praktyka programistyczna wymagająca od deweloperów integracji kodu ze wspólnym repozytorium kilka razy dziennie. Każde zatwierdzenie (check-in) jest następnie weryfikowane przez zautomatyzowane budowanie (build), co pozwala zespołom na wczesne wykrywanie problemów.”
Problem “udaneqo produktu”
Kiedy myślimy o nowym produkcie programistycznym, jednym z istotnych wczesnych kroków w procesie jest stworzenie prototypu. W tym momencie zazwyczaj powstaje tylko wstępna baza kodu (być może zawierająca podstawowe testy). Choć niektórzy mogą sądzić, że to wystarczy, autor zdecydowanie zaleca wcześniejsze stworzenie kilku dodatkowych elementów procesu:
- Przynajmniej podstawowa dokumentacja i szkic architektury
- Plany testów na różnych poziomach (testy jednostkowe, dymne, integracyjne, obciążeniowe itp.)
- Reguły analizy statycznej
- Reguły inspekcji kodu
- Założenia dotyczące bezpieczeństwa
- i inne specyficzne dla produktu
W świecie rzeczywistym możemy napotkać problemy: gdy produkt rośnie, czas potrzebny na wykonanie wszystkich wymaganych kroków w naszym procesie wydłuża się. Jednak łatwiej i zdecydowanie taniej jest stworzyć poprawną strukturę, metody i narzędzia na początku cyklu życia projektu.
Niekontrolowany wzrost ilości testów
Ogólnie rzecz biorąc: dobrze jest mieć jak najwięcej testów na każdym poziomie testowania. Jednak nie zawsze jest prawdą, że ilość przekłada się bezpośrednio na jakość. Jeśli czas wykonania testów jest zbyt długi, generuje to ryzyko, że użytkownicy będą próbowali pominąć wykonywanie testów zamiast je usprawnić. Rozwiązaniem może być tutaj uporządkowanie zestawu testów w różne poziomy zaufania i niewykonywanie wszystkich zestawów testów po każdym commitcie. Czasochłonne przypadki testowe można przenieść na inny poziom zaufania.
Jakość testów
Błędy w wykonaniu testów nie są akceptowalne. Przypadek testowy musi mieć jasny wynik – prawda lub fałsz. Fałsz oznacza, że albo istnieje błąd w oprogramowaniu, który musi zostać naprawiony, albo test nie jest dostosowany do nowych wymagań. Utrzymywanie takich “psujących się” testów jest niedopuszczalne. Generuje to chaos decyzyjny, prowadzący do pytań typu: “Mamy 85% pokrycia testami, ale te brakujące 15% zawsze oblewało… Więc, czy możemy wydać produkt?”. Nie ma odpowiedzi na takie pytanie, ponieważ każde pojedyncze niepowodzenie testu jest potencjalnym błędem oprogramowania. Może ono występować latami z wynikiem negatywnym i wszyscy wiedzą, że “zawsze oblewa”… Ale takie błędy mogą być mylące, ponieważ pewnego razu test może nie przejść z zupełnie nowych powodów.
Czas wykonywania testów
Liczba testów rośnie wraz z rozmiarem produktu. Koncepcja poziomów zaufania do oprogramowania może pomóc w kategoryzacji przypadków testowych. Te, które są najbardziej czasochłonne, mogą zostać przeniesione na inny poziom zaufania i wykonywane rzadziej (lub w innym środowisku testowym).
Poziom zaufania dla określenia jakości
Trunk Based Development (nowoczesne podejście)
Dziś, w erze Trunk-Based Development, idea poziomów zaufania do oprogramowania ewoluowała. Zamiast fizycznie przenosić kod między gałęziami (branchami), przesuwamy build przez kolejne etapy zautomatyzowanego potoku (pipeline). Jednak logika stojąca za “bramkami jakości” pozostaje ta sama: nie pozwalamy kodowi przejść dalej, dopóki nie osiągnie on odpowiedniego Poziomu Zaufania.
Mapujemy poziomy zaufania na etapy w Pipeline CI/CD:
- Pull Request / Pre-merge: -> Środowisko deweloperskie (Dev).
- Post-merge / Staging: -> Wdrożenie na środowisko testowe.
- Produkcja (z wyłączoną flagą funkcjonalności): -> Wdrożenie typu Canary, testy na produkcji.
- Produkcja (z włączoną flagą funkcjonalności): -> Pełna dostępność dla użytkowników.
Model kaskadowy (wiekowy, ale wciąż żywy GitFlow)
Załóżmy, że zadania w naszym projekcie są dystrybuowane do kilku zespołów deweloperskich. Aby zachować wymagania, konieczne jest posiadanie odpowiedniej struktury repozytorium kodu. Jedną z możliwych opcji jest model kaskadowy:
- Gałąź główna (master): Zawiera kod gotowy do produkcji.
- Gałęzie projektowe: Wywodzące się z mastera, używane do rozwijania dodatkowych funkcji produktu.
- Gałęzie prywatne: Wywodzące się z gałęzi projektowych, używane do indywidualnego rozwijania funkcjonalności.
Modele kaskadowe są nadal w użyciu. Pozwalają deweloperom pracować niezależnie od siebie i upraszczają operacje dostarczania i scalania (merge) kodu.
Elementy zaufania do oprogramowania
Aby utrzymać czystość repozytorium, organizacja musi opracować jasne zasady. Na przykład projekt może ustanowić kilka różnych reguł jakości wynikających z:
- Analizy statycznej
- Testów jednostkowych
- Manualnej inspekcji kodu
- Testów wydajnościowych
- Testów bezpieczeństwa
Podział punktów walidacji
Nie możemy uruchamiać pełnej regresji przy każdym commitcie. Musimy podzielić reguły w oparciu o złożoność i czas wykonania.
- Szybki test (np. “test dymny” - smoke test) - poziom 1: Specjalnie przeglądowe testy, które dają wyniki bardzo szybko.
- Normalny test - poziom 2: Bardziej złożone sprawdzenia, zwiększenie złożoności skutkuje dokładniejszym i dłuższym testem.
- Test długotrwały - poziom 3: Najwyższy możliwy poziom zaufania, akceptowany dłuższy czas wykonania testóſ.
Implementacja poziomów zaufania
Załóżmy, że chcemy dać “zielone światło” deweloperowi na scalenie kodu. Ogólna wskazówka brzmi: zdefiniuj co najmniej tyle poziomów zaufania, ile masz typów gałęzi w repozytorium.
Podstawowa zasada dla każdego Pull Requesta nakładałaby wymóg osiągnięcia określonego poziomu zaufania, zanim kod będzie mógł zostać scalony.
Przykładowy Scenariusz:
- Poziom 1: Jeśli testy podstawowe, testy dymne i inspekcja kodu zakończą się sukcesem, produkt osiąga pierwszy poziom zaufania. Możemy scalać do gałęzi projektowej.
- Poziom 2: W środowisku testowym wykonujemy analizę statyczną i testy regresji. Jeśli zakończą się sukcesem, produkt osiąga drugi poziom zaufania.
- Poziom 3: W środowisku stagingowym uruchamiamy testy wydajnościowe i długotrwałe. Jeśli zakończą się sukcesem, kod jest gotowy do Testów Akceptacyjnych Użytkownika (UAT) i wdrożenia.