Model V w testowaniu oprogramowania

19 october 2023
Jakub Rojek Jakub Rojek
Zdjęcie autorstwa Edward Jenner z Pexels (https://www.pexels.com/pl-pl/zdjecie/mezczyzna-osoba-technologia-obiektyw-4033151/)
Categories: Programming, Quality Assurance

Każdy wie o tym, że jeśli realizuje się cokolwiek, to po wykonaniu wypada swoje dzieło sprawdzić. Nie inaczej jest w przypadku oprogramowania - wszyscy doskonale znamy pojęcie "buga" i ogólnie błędów w programach. Niektórzy doświadczyli ich w samym Windowsie (lub macOS), inni w różnych programach biurowych, a jeszcze inni z tym słowem zetknęli się grając w różne gry, gdzie napotkanie niedoskonałości kodu mogło skończyć się śmiechem lub gniewem. Nikt nie lubi błędów i najchętniej wszyscy byśmy się ich pozbyli z przeróżnych aplikacji, nawet jeśli są pocieszne (błędy, nie aplikacje). Czy można to zrobić? Nie całkiem, ale są techniki, które w tym pomagają.

Oczywiście, program komputerowy jest zapisany w postaci kodu, a więc sekwencji instrukcji. Jest to coś, co można przedstawić w sposób matematyczny, a to z kolei sprawia, że istnieje teoretyczna możliwość formalnego sprawdzenia poprawności takiego programu. Jednak, poza zapewne nielicznymi przypadkami, tego się nie robi z prostego powodu - jest to trudne i niemożliwe z praktycznego punktu widzenia, gdy aplikacje są obecnie gigantyczne, asynchroniczne, podzielone na wiele składowych (w tym mikroserwisy) i zależą od mnogości parametrów.

Pozostają zatem inne metody, w tym statyczne (do których zaliczają się między innymi przeglądy kodu) oraz dynamiczne. Wśród tych ostatnich mamy do czynienia z testowaniem, o którym dzisiaj skrótowo sobie porozmawiamy, pokazując, że nie dotyczy ono tylko jednego poziomu oprogramowania. A przy okazji rozjaśnimy sobie parę pojęć.

Czym jest model V?

Przejdźmy od razu do bohatera dzisiejszego tekstu, a mianowicie koncepcji, która nazywa się modelem V. Żeby jednak lepiej go zrozumieć, najpierw musimy podzielić sobie testy na manualne oraz automatyczne. Te pierwsze są dość proste koncepcyjnie - jest człowiek, który "przeklikuje" aplikację, sprawdzając różne przypadki ręcznie i wypisując błędy, które zauważy. Oczywiście, wiele osób krzyknie teraz, że to w ogóle bez sensu, gdyż wszystko powinno się weryfikować automatycznie. Poniekąd mają rację - testy ręczne mają swoje duże wady, wśród których najczęściej wymienia się czas ich przeprowadzania, brak powtarzalności oraz czynnik ludzki w postaci testera, który może się zmęczyć lub czegoś nie zauważyć. Ale, wbrew pozorom, manualne testy nadal mają sens, gdyż o ile w długiej perspektywie ich czas realizacji jest dłuższy niż automatów, o tyle do szybkiej pojedynczej weryfikacji jak najbardziej się nadają i można do niej przystąpić z marszu. Nie wymagają też żadnego przygotowania od strony kodu, a na dodatek pozwalają testować aplikację z perspektywy normalnego użytkownika, co często jest bardzo istotne i jeszcze do tego wrócimy.

Z drugiej strony nie można zaprzeczyć zaletom testów automatycznych - znacząco ułatwiają ponowną weryfikację, a także (a może i przede wszystkim) pozwalają robić tzw. testy regresyjne, a więc sprawdzenie, czy nowe zmiany nie naruszyły istniejących funkcji. Istnieją też jednak wady - wymagają odpowiednich bibliotek, zakodowania i w przypadku, kiedy coś weryfikowane jest raz, mogą zająć więcej czasu niż ręczne testowanie (inna sprawa, że nigdy nie jesteśmy w stanie przewidzieć, czy do weryfikacji czegoś jeszcze nie wrócimy). Do tego nie są takie proste przy testowaniu interfejsu użytkownika, a przynajmniej nie wszystko pozwolą zweryfikować. Tym niemniej, to tak użyteczne narzędzie, że nie sposób go pominąć i dodatkowo stanowi podstawę tego, do czego dążymy, a więc modelu V.

Bloczki układające się w literę V. Od góry - po lewej jest \

Zerkając w różne opracowania, znajdziecie różniące się od siebie przedstawienia tego modelu. Czasem brakuje jednego stopnia, innym razem układ jest nieco inny, natomiast wszystkie sprowadzają się do tego, że testowanie zachodzi na różnych poziomach, z których każdy odpowiada konkretnego etapowi tworzenia oprogramowania. Przyjrzyjmy się im od dołu, po kolei.

To, z czym większość kojarzy samo programowanie, to tworzenie plików z kodem i klasami w paradygmacie obiektowym, które zawierają pewną wydzieloną część całego programu. Przykładowo, możemy mieć plik reprezentujący jakiś obiekt, element logiki biznesowej, kontroler czy klasę pomocniczą. W każdym znajdują się jakieś funkcje czy metody, przyjmujące określone parametry i zwracające dla nich konkretny wynik (pomijając kwestię losowości). Możemy się spodziewać, że jakość oprogramowania zależy między innymi od tego, czy wszystkie metody działają prawidłowo, a więc każdą z nich trzeba przetestować. To robi się testami jednostkowymi (ang. unit tests), które każdą metodę pozwalają zweryfikować pod kątem różnych przypadków, zwłaszcza brzegowych oraz wyjątkowych (a więc takich, w których programista mógł nie dopatrzyć czegoś lub źle zinterpretować). To są testy przeprowadzane najczęściej i są trudne do zrobienia w inny efektywny sposób sposób niż automatyczny.

Wróćmy do tego, że jakość oprogramowania zależy od tego, czy wszystkie składniki zachowują się poprawnie. Teraz zaprzeczę sam sobie, ponieważ nie do końca tak jest - może się okazać, że wszystkie metody klas w ramach danego modułu działa dobrze, ale nie są właściwie wywoływane lub nie wpływają na siebie odpowiednio (czy też wpływają na siebie aż za bardzo). W sieci można znaleźć dużo żartobliwych przykładów takiej sytuacji, często w postaci gifów:

Dlatego większe fragmenty architektury oprogramowania, czyli zestawy modułów lub wręcz całe architektury sprawdza się za pomocą testów integracyjnych. Działają one tak samo, jak testy jednostkowe i też zwykle są zautomatyzowane, natomiast skupiają się nie na metodach (gdzie jeden test testuje konkretny przypadek metody), ale raczej całych sekwencjach zdarzeń, spójności poszczególnych obiektów ze sobą i kolejności wywoływania poszczególnych elementów.

Idąc dalej musimy pamiętać o tym, że architektura systemu jest tworzona po to, aby zaspokoić wymagania funkcjonalne i pozafunkcjonalne. Te podlegają testom systemowym (funkcjonalnym) oraz testom pozafunkcjonalnym - jak można spodziewać się po nazwach, odpowiadają one poszczególnym typom elementów zaimplementowanych w oprogramowaniu. Dużo ciekawsze są testy pozafunkcjonalne, gdyż pozwalają zweryfikować np. wydajność, obciążenie czy bezpieczeństwo systemu, a więc kwestie, których nie da się zbadać prostymi metodami i w każdych warunkach. Część weryfikacji można przeprowadzić ręcznie (jak np. niektóre punkty związane z bezpieczeństwem), ale trudno wyobrazić sobie, aby takę ścieżkę przyjąć w przypadku obciążenia - nie będziemy prosić np. wszystkich studentów, aby w jednym momencie weszli na stronę. Na szczęście, są takie narzędzia jak JMeter czy k6, które mogą w tym pomóc.

Ostatni, najwyższy poziom to nadal wymagania, ale rozumiane trochę szerzej - jak najbardziej mamy tutaj do czynienia z testowaniem wymagań funkcjonalnych, ale bardziej chodzi o przepływ użytkownika i przy okazji walidację tego, czy wszystko jest w porządku z "ludzkiego" punktu widzenia. Tutaj może warto na chwilę się zatrzymać i wyjaśnić, że weryfikacja i walidacja to dwa różne pojęcia. Pierwsze polega na sprawdzeniu, czy oprogramowania działa zgodnie z wymaganiami, a więc można powiedzieć, czy wszystkie punkty są odhaczone i wszystko zgadza się "matematycznie". Natomiast walidacja to "test" tego, czy oprócz poprawnej implementacji wszystko działa tak, jak oczekuje klient i spełnia jego, czasem niewypowiedziane wcześniej, wymogi. Co z tego bowiem, że w systemie jest możliwość dodania produktu do koszyka, wyświetlenia tego koszyka i zamówienia produktu, jeśli żadna z tych funkcji nie jest łatwo dostępna i nie tworzą razem scenariusza, który wręcz narzuca się użytkownikowi będącemu na stronie. Można więc powiedzieć, że mamy powtórkę z kodu - każde wymaganie może być zaimplementowane idealne, ale razem nie tworzą dobrego systemu. Za sprawdzenie tego odpowiadają testy akceptacyjne, które często są tworzone bardzo wcześnie (wręcz przy okazji tworzenia wymagań) i to przy udziale klienta. Wtedy najczęściej mówimy o manualnych testach akceptacyjnych (tzw. MATach) lub ich prostszej, mniej formalnej formie - punktach Definition of Done, które stanowią drogowskaz dla programisty. I nie da się ukryć, że tutaj akurat ręczne testy często okazują się jeśli nie lepsze, to przynajmniej potrzebne, z uwagi na walidację i ludzką ocenę sytuacji. Natomiast należy pamiętać, że jak najbardziej istnieje możliwość testowania na tym poziomie w sposób automatyczny, do czego najpopularniejszym (aczkolwiek niejedynym) narzędziem jest Selenium, dostarczające zarówno interfejs graficzny, jak i API.

Testy testom nierówne

Warto jeszcze wspomnieć o paru pojęciach, które nieodłącznie kojarzą się z testowaniem. O tym, czym się różni weryfikacja od walidacji wspomniałem już wyżej. Teraz czas jeszcze wprowadzić termin czarnej i białej skrzynki. Pierwszy z nich (czyli popularne "black boxy") to takie testy, jakie może przeprowadzić każdy z nas - nie znając tego, co jest w środku, korzystamy z oprogramowania i patrzymy, jakie dostarcza wyniki. Nie musimy wiedzieć, jak wygląda implementacja pod spodem - liczy się dla nas tylko to, jakie są parametry wejściowe oraz wyjściowe. Z kolei "white boxy" biorą pod uwagę również implementację - tester wie, jak zbudowana jest dana metoda i gdzie można spodziewać się większych problemów, przez co może skupić się na ich weryfikacji. Obie ścieżki mają swoje zalety i wady - z jednej strony czasem lepiej się weryfikuje nie wiedząc, co dzieje się w aplikacji, a z drugiej mając tę świadomość można lepiej wycelować testy lub wręcz zauważyć gołym okiem jakieś braki.

Trzeba też wiedzieć, że testowanie nie dostarcza dowodu na to, że oprogramowanie działa poprawnie. Ono wzmacnia naszą pewność, że tak jest, natomiast siłą rzeczy weryfikacja oparta o różne przypadki testowe nie jest w stanie pokryć wszystkich możliwości, jakie mogą wystąpić na przestrzeni lat i - matematycznie ujmując - zweryfikować całej dziedziny argumentów. Nie mówiąc już o tym, że pod wpływem różnego obciążenia czy nowej wersji narzędzi, na których użytkownik uruchamia oprogramowanie, też mogą ujawnić się różne "file" i błędne działanie niewynikające z samych pomyłek programistycznych. Dlatego testować jak najbardziej warto (w jakikolwiek sposób), ale to nie jest tak, że 100% testów ukończonych poprawnie świadczy o bezbłędnym oprogramowaniu.

Warto też pamiętać, że testować należy jak najwcześniej - to dlatego w ogóle tworzymy wymagania oraz prototypy, aby pewne kwestie przetestować niskim kosztem. Im poźniej wystąpi dany błąd, tym zwykle więcej kosztuje jego naprawienie i to nie tylko budżetowo, ale też nerwowo. Stresów podczas tworzenia oprogramowania jest co niemiara - warto zabezpieczyć się przed ich dokładaniem sobie. A na wypadek, gdyby jakieś błędy przemknęły do dalszych prac lub pojawiły się nowe propozycje, warto skonfigurować sobie odpowiedni system.

Kto powinien testować?

Pytanie wydaje się głupie, gdyż wszyscy zaraz krzykną, że od tego jest dział Quality Assurance, czyli popularny QA. Jednak to tylko część prawdy. Rzeczywiście, zespoły IT często dysponują testerami, którzy zdejmują z programistów obowiązek weryfikowania każdego przypadku i są bezstronni. Jednak należy pamiętać o kilku rzeczach.

Po pierwsze, nie każda firma ma dział QA, choćby ze względów budżetowych. Po drugie, testerzy nie będą w stanie dobrze weryfikować programów, jeśli nie będą znali ich wymagań oraz oczekiwań klienta. Po trzecie, bycie testerem to wbrew pozorom dość trudna, a przede wszystkim żmudna praca, która wymaga uwagi oraz zwracania uwagi na szczegóły. Z tego powodu czasem mówi żartobliwie mówi się, że dział QA nie powinien lubić się z działem programistów, aby z większą zaciekłością szukać bugów i tym samym czynić oprogramowanie lepszym.

To wszystko sprawia, że nie można wszystkiego zrzucać na testerów i siłą rzeczy programista powinien w pewnym zakresie upewnić się, że jego kod działa poprawnie i spełnia różne, choćby podstawowe lub prawdopodobne przypadki. Pierwsza sprawa to zwykła ludzka przyzwoitość - w przeszłości słyszałem o przypadkach, w której koder wypuszczał oprogramowanie, które "wydaje mu się, że działa", licząc na to, że ktoś wyłapie błędy, bo jemu samemu nie chce się testować lub twierdzi, że nie ma na to czasu. To już nie tylko zła praktyka, ale też po prostu wstyd i brak poszanowania cudzej pracy. Druga kwestia to fakt, że o ile programista nie wszystko jest w stanie znaleźć będąc "zafiksowanym" na danym kodzie, to właśnie bycie tak blisko swojego kodu sprawia, że jest w stanie szybko i na gorąco poprawić oczywiste błędy, które widać po kilku przebiegach kodu. Później dotarcie do źródeł problemu będzie trudniejsze.

Omawiany model V pokazuje też inne stopnie testowania i tak np. testy akceptacyjne nie są tak naprawdę techniczne - to bardziej sprawdzenie oczekiwań klienta względem systemu. A więc mogą być przeprowadzane także zarówno przez osoby reprezentujące zespół IT, jak i samego klienta, który tylko w ten sposób może przekonać się, czy rozumie to, jak działa system oraz czy software house prawidłowo zrozumiał jego kontekst biznesowy.

Podsumowanie

To zaledwie wierzchołek góry lodowej tematu związanego z testowaniem oprogramowania, a przecież tak, jak pisałem - to tylko jedna z metod zapewniania jakości aplikacji. Model V może kształtować się trochę inaczej w zależności od potrzeb i nie ma potrzeby też ścisłego trzymania się tego podziału oraz pokrywania każdego stopnia we wszystkich projektach. Tym niemniej, stanowi poradnik, pozwalający dobrze podejść do weryfikacji oraz walidacji wykonywanej pracy.

Pozdrawiam i dziękuję - Jakub Rojek.

We like to write, even a lot, but on a daily basis we develop web and mobile applications. Check some of the programs we have made.

About author

Jakub Rojek

Lead programmer and co-owner of Wilda Software, with many years of experience in software creation and development, but also in writing texts for various blogs. A trained analyst and IT systems architect. At the same time he is a graduate of Poznan University of Technology and occasionally teaches at this university. In his free time, he enjoys playing video games (mainly card games), reading books, watching american football and e-sport, discovering heavier music, and pointing out other people's language mistakes.

Jakub Rojek