Projektowanie pod kątem testowalności: przełamywanie kolejnej barier
4 min czytania

W naszej bieżącej serii DevOps for the Win dokumentujemy naszą drogę transformacji DevOps. W rozdziale 1 przedstawiliśmy Trójkąt DevOps: Narzędzia, architektura i ludzie w tworzeniu produktów, w którym omówiliśmy elementy podstawowe napędzające sukces DevOps. W rozdziale 2 przyjrzeliśmy się zagadnieniu Pierwsze kroki w naszej transformacji DevOps, dzieląc się tym, jak przygotowaliśmy grunt pod znaczące zmiany.

Teraz w Rozdziale 3 poruszamy kolejne duże wyzwanie, z którym się mierzyliśmy: Projektowanie pod kątem testowalności. Po ulepszeniu struktury naszego zespołu, dopracowaniu definicji ukończenia prac (Definition of Done) i wdrożeniu zautomatyzowanej analizy kodu zdaliśmy sobie sprawę, że głównym wąskim gardłem w naszym procesie stało się projektowanie oprogramowania, a konkretnie jego testowalność. Praca piętrzyła się w fazie testów, co uniemożliwiało nam skrócenie cykli wydawniczych. Ten rozdział zagłębia się w to, jak pokonaliśmy to wyzwanie i poprawiliśmy naszą zdolność do wydajnego dostarczania wysokiej jakości oprogramowania.

Wąskie gardło w testowaniu

Głównym powodem tego opóźnienia było to, że testowanie miało charakter przede wszystkim ręczny, koncentrując się na testowaniu interfejsu użytkownika i przepływach pracy użytkownika. Ponieważ interfejs użytkownika był zazwyczaj jednym z ostatnich elementów do ukończenia, automatyzacja niemal nie istniała, a testowanie ręczne stało się jedynym wykonalnym podejściem. Zautomatyzowane przypadki testowe interfejsu użytkownika (np. testy Selenium) były zazwyczaj pisane po zakończeniu testów ręcznych, głównie do celów regresji.

Poleganie na ręcznym testowaniu skutkowało:

  • znaczącymi opóźnieniami w wydawaniu oprogramowania.
  • Konieczność wielu wdrożeń umożliwiających równoległe testowanie przez więcej niż jednego członka zespołu QA.

Wskazało to na znaczenie zwiększenia testowalności kodu w celu optymalizacji przepływu wartości. Okazało się jednak, że osiągnięcie tego celu jest trudniejsze, niż zakładaliśmy.

Priorytetowe traktowanie testów API i testów jednostkowych

Aby stawić czoła temu wyzwaniu, skupiliśmy się na dwóch głównych obszarach:

  1. Testy jednostkowe – zapewnienie możliwości testowania poszczególnych komponentów w izolacji.
  2. Testy API – zmniejszenie zależności od testów opartych na interfejsie użytkownika i bazie danych.

Testowanie API: Przesunięcie ku wczesnemu testowaniu w celu uzyskania szybszych informacji zwrotnych

Zdefiniowaliśmy na nowo naszą strategię testowania API, wprowadzając następujące kluczowe ulepszenia:

  1. Dobrze zdefiniowane APIs – APIs zostały zaprojektowane tak, aby były proste, dobrze udokumentowane i dostępne na wczesnym etapie rozwoju.
  2. Unikanie bazy danych jako punktu integracji – opieranie się na bazach danych w procesie integracji tworzyło zależności, które spowalniały testowanie. Zamiast tego przeszliśmy na integrację opartą na API z użyciem protokołu REST przez HTTP oraz GraphQL. Pozwoliło to zminimalizować czas konfiguracji bazy danych i usprawnić automatyzację testów.

Ta zmiana znacznie ograniczyła opóźnienia, umożliwiając szybszą automatyzację oraz testowanie na wczesnym etapie, co pokazało, że skupienie się na projektach zorientowanych na API poprawiło zarówno testowalność, jak i wydajność.

Testy jednostkowe: Zmiana sposobu myślenia

Początkowo nasze wysiłki związane z testami jednostkowymi prowadziły do szybkiego wzrostu pokrycia kodu testami, jednak szybko zidentyfikowaliśmy problemy:

  • Niektóre testy miały charakter powierzchowny i były pisane jedynie po to, aby spełnić cele dotyczące pokrycia kodu.
  • Brakowało w nich istotnej walidacji funkcjonalności, przypadków brzegowych oraz scenariuszy błędów.

Aby temu przeciwdziałać, położyliśmy nacisk na:

  • Edukację programistów w zakresie pisania wartościowych testów.
  • Refaktoryzacja kodu w celu poprawy jego testowalności.

Wyzwania związane z pisaniem kodu podlegającego testowaniu

Największą przeszkodą w skuteczny testowaniu jednostkowym był brak możliwości testowania w samej bazie kodu. Główne problemy obejmowały:

  • Duże, monolityczne funkcje.
  • Silnie powiązane komponenty.
  • Słabe oddzielenie poszczególnych kwestii.
  • Niewystarczająca abstrakcja.

Kwestie te utrudniały skuteczną izolację i testowanie poszczególnych elementów. Aby to rozwiązać, wykonaliśmy następujące czynności:

  1. Zapewnione wytyczne: Podzieliliśmy się najlepszymi praktykami dotyczącymi:
    • Wybór funkcji do testów jednostkowych.
    • Refaktoryzacja kodu w celu poprawy jego testowalności.
    • Projektowanie i używanie obiektów zastępczych (mocków) w celu ułatwienia testowania.
  2. Skupienie na stopniowych ulepszeniach: Zachęcano programistów do wprowadzania małych, znaczących zmian w celu poprawy testowalności w czasie.

Powolny, ale stały postęp

Mimo tych wysiłków postęp był powolny, zwłaszcza w przypadku starszych produktów. Istniejąca architektura ograniczała liczbę testów jednostkowych, jakie mogliśmy napisać. Działania te nie miały jednak na celu jedynie poprawy obecnego kodu – stanowiły inwestycję w przyszłość:

  • Rozwijanie umiejętności programistów w zakresie projektowania testowalnego kodu zapewniło, że przyszłe projekty nie ucierpią z powodu tych samych problemów.
  • Stopniowe ulepszenia zapobiegały zakłóceniom, stale zwiększając zakres automatyzacji testów.
  • Zmiana sposobu myślenia pomogła zespołom dostrzec w testowalności nie obciążenie, lecz konieczność zapewnienia trwałego sukcesu wdrożeniowego w metodyce DevOps.

Dla zespołów i organizacji rozpoczynających transformację DevOps testowalność jest kluczowym obszarem zainteresowania. Pomaga ona łagodzić wąskie gardła w fazie tworzenia oprogramowania. Ważne jest jednak zarządzanie oczekiwaniami – natychmiastowe rezultaty mogą nie być możliwe, zwłaszcza w przypadku pracy z dużymi, starszymi zasobami kodu. Refaktoryzacja takiego kodu bez odpowiedniej liczby testów jednostkowych i regresyjnych stanowi poważne wyzwanie.

Lekcje z książki The Pragmatic Programmer

W sytuacjach, gdy rozpoczęcie pracy z nową bazą kodu nie jest możliwe, publikacja autorstwa Andy'ego Hunta i Davida Thomasa pt. The Pragmatic Programmer: Your Journey to Mastery oferuje praktyczne wskazówki:

  • Celowe zmiany przyrostowe: Skup się na przebudowie małych, łatwych do opanowania fragmentów kodu.
  • Rozdzielenie komponentów: Zmniejsz liczbę powiązań, aby poszczególne elementy było łatwiej testować.
  • Rozbijanie dużych funkcji: Podziel duże funkcje na mniejsze, bardziej skoncentrowane jednostki.
  • Wdrożenie projektowania modułowego: Spraw, aby kod był łatwiejszy w testowaniu i utrzymaniu poprzez poprawę modułowości.

Przekaz jest jasny: po usunięciu początkowych wątki haseł, testowalność powinna stać się kluczowym obszarem zainteresowania. Ułatwi to pracę w fazie testów i pozwoli szybciej dostarczać oprogramowanie wysokiej jakości.

Patrząc w przyszłość

Nasza droga do poprawy testowalności nauczyła nas cennych lekcji na temat roli projektowania oprogramowania w umożliwianiu płynnej transformacji DevOps. Skupiając się na testowalności, usunęliśmy wąskie gardła, podnieśliśmy umiejętności programistów i położyliśmy fundament pod przyszły sukces. Chociaż natychmiastowe rezultaty mogą być powolne, długoterminowe korzyści w postaci jakości, wydajności i rozwoju zespołu sprawiają, że ta inwestycja jest tego warta.

W miarę kontynuowania naszej drogi z DevOps, mierzenie sukcesu staje się kolejnym kluczowym wyzwaniem. W Rozdziale 4 przyjrzymy się, jak wdrożyliśmy wskaźniki KPI i pulpity nawigacyjne, aby śledzić nasze postępy i wskazywać kolejne obszary do poprawy.

Śledź nas uważnie, gdyż będziemy omawiać, jak podejmowanie decyzji w oparciu o dane pomogło nam udoskonalić nasze procesy DevOps i zoptymalizować wydajność!

Subskrybuj bloga Freyr

Polityka prywatności