Jedną z najbardziej znanych i kosztownych awarii oprogramowania była katastrofa rakiety Ariane 5. W dniu 4 czerwca 1996, po niespełna 40 sekundach dziewiczego lotu, rakieta gwałtownie zboczyła ze swojej trajektorii, a siły aerodynamiczne spowodowały oderwanie się rakiet bocznych. Uruchomiło to automatycznie procedurę samozniszczenia. Straty finansowe sięgnęły miliardów dolarów.
Winowajcą katastrofy Ariane 5 okazał się być defekt kodu – oprogramowanie nie przewidywało zabezpieczenia w przypadku błędu konwersji liczby z formatu 64-bitowego do 16-bitowego. Konwertowana zmienna miała zbyt dużą wartość, by zapisać ją w za pomocą 16 bitów, czego efektem było wystąpienie awarii. Spowodowała ona, że dane diagnostyczne zostały błędnie zinterpretowane przez komputer pokładowy jako dane lotu i użyte do wyliczenia dalszej trajektorii. Co ciekawe, kawałek kodu odpowiedzialny za zmianę formatu liczb został przeniesiony z oprogramowania dla poprzedniej wersji rakiety, której parametry znacząco różniły się od tych dla nowszego modelu.
Podobny problem, choć może nie tak drastyczny w skutkach, wystąpił kilka miesięcy także w naszym kraju. 17 marca 2022, przed godziną 4 rano, systemy sterowania ruchem pociągów PKP doznały awarii w 19 lokalizacjach na terenie Polski. Kłopoty dotknęły również Indie, Tajlandię, Peru, Włochy, Szwecję i Holandię. Ruch kolejowy został sparaliżowany, pociągi miały wielogodzinne opóźnienia lub były odwoływane. Firma Alstom, odpowiedzialna za niedziałające urządzenia, wydała komunikat, że powodem problemów była usterka w oprogramowaniu – błąd w formatowaniu czasu.
Obu tym awariom prawdopodobnie można by zapobiec, gdyby przeprowadzono bardziej dogłębne testy oprogramowania przed wypuszczeniem go na rynek. Piszę prawdopodobnie, ponieważ należy pamiętać, że (zgodnie z pierwszą z siedmiu zasad testowania) testowanie ujawnia usterki, ale nie może dowieść ich braku. Z całą pewnością możemy jednak stwierdzić, że testowanie przyczynia się do obniżenia poziomu ryzyka wystąpienia awarii. Dodatkowo, wczesne testowanie pozwala zapobiec lub ograniczyć konieczność wprowadzania kosztownych (pieniężnie i czasowo) zmian na późniejszych etapach cyklu życia oprogramowania. Dla przykładu, poprawa błędnego założenia podczas precyzowania wymagań dla oprogramowania jest znacznie łatwiejsza niż modyfikacja funkcjonującego już kodu.
Słysząc o testowaniu, przeciętnej osobie przyjdą do głowy przede wszystkim testy polegające na uruchomieniu oprogramowania i identyfikacji występujących awarii. Takie testowanie nazywamy testowaniem dynamicznym. Skupia się ono na zewnętrznym zachowaniu systemu, czyli na tym, z czym ma do czynienia docelowy użytkownik. Jest to rodzaj testowania pojawiający się zdecydowanie najczęściej podczas mojej codziennej pracy – w końcu każda wprowadzona przez deweloperów poprawka powoduje konieczność sprawdzenia, czy awaria spowodowana przez naprawiony defekt rzeczywiście już nie występuje, a każda wdrożona nowa funkcjonalność wymaga skontrolowania, czy aplikacja istotnie spełnia założone wymagania. Ważną kwestią są też testy regresji, które pozwalają upewnić się, że zaimplementowane zmiany nie spowodowały pojawienia się nowych defektów lub odkrycia tych już istniejących w obszarze oprogramowania, który nie podlegał modyfikacjom.
Sam proces testowania jest oczywiście w mniejszym lub większym stopniu sformalizowany. Test przeprowadza się w oparciu o tzw. przypadek testowy, którego tworzenie i ewentualna aktualizacja również należy do obowiązków testera. Przypadkiem testowym nazywamy, wedle definicji, zestaw warunków wstępnych, danych wejściowych, akcji (w stosownych przypadkach), oczekiwanych rezultatów i warunków końcowych opracowany w oparciu o warunki testowe. W praktyce oznacza to, że dla każdej funkcjonalności - np. poprawnego logowania się do aplikacji - powinniśmy najpierw zdefiniować wymagania konieczne do przeprowadzenia testu – np. użytkownik jest zarejestrowany, jego konto jest aktywne, jest wylogowany z aplikacji, znajduje się na ekranie logowania. W naszym przykładzie danymi wejściowymi będzie login i hasło użytkownika. Akcje to kolejne kroki, które należy wykonać – wpisanie loginu i hasła w odpowiednie pola formularza i kliknięcie przycisku logowania. Oczekiwanym rezultatem będzie poprawne zalogowanie się jako podany użytkownik, przekierowanie do strony głównej, wyświetlenie komunikatu o pomyślnym logowaniu. Nasz warunek końcowy to: użytkownik jest zalogowany.
Pojedyncze przypadki testowe często łączy się w większe zestawy. W projekcie CRM Qompana, nad którym pracujemy w Engave, całkowita liczba przypadków testowych idzie w tysiące. Przejście przez każdy z nich byłoby bardzo czasochłonne, dlatego w przypadku wcześniej wspomnianych testów regresji stworzyliśmy zestaw testowy ograniczony do przypadków o największym priorytecie. Obejmuje on podstawowe funkcjonalności aplikacji i typowe ścieżki przechodzone przez użytkownika podczas korzystania z systemu. Przeprowadzenie testów regresji na podstawie tego zestawu pozwala w relatywnie krótkim czasie dosyć trafnie ocenić, czy wprowadzone zmiany negatywnie wpłynęły na dotychczasową funkcjonalność aplikacji. Tworzenie takich pakietów krytycznych scenariuszy w przypadku testów regresji jest działaniem typowym dla projektów Agile prowadzonych w metodyce Scrum, gdzie częste przyrosty funkcjonalności oprogramowania wymuszają równie częste przeprowadzanie testów regresji. Oczywiście, aby oszczędzić sobie pracy i czasu, w dzisiejszych czasach dąży się do automatyzacji testowania. Najlepszymi kandydatami do automatyzacji są te przypadki, które wykorzystuje się wielokrotnie – czyli między innymi właśnie testy regresji.
Równie ważnym aspektem jest również testowanie statyczne, które pozwala wykryć defekty bezpośrednio w produktach pracy, takich jak wymagania, kod czy specyfikacje. Dla przykładu, jako tester jestem zaangażowana w sprawdzanie projektów graficznych aplikacji. Dzięki temu już na tak wczesnym etapie prac jestem w stanie wykryć choćby niespójności w wyglądzie przycisków czy braki kluczowych pól w formularzach. Testowanie statyczne kodu pozwala znaleźć takie defekty jak powielony kod, zadeklarowane, ale nieużywane zmienne czy zmienne o niezdefiniowanej wartości. Testowanie statyczne wymagań pomaga natomiast odkryć wszelkie niejednoznaczności, sprzeczności czy elementy nadmiarowe.
Zaletą testowania statycznego niewątpliwie jest to, że pozwala ono ujawnić defekty, do których nie moglibyśmy dotrzeć poprzez testowanie dynamiczne, ponieważ nie powodują one awarii, albo ścieżki, na których się znajdują, są trudno dostępne. Testowanie statyczne stosowane przed rozpoczęciem testowania dynamicznego, na początkowych etapie cyklu życia oprogramowania, przyczynia się do oszczędności czasu i finansów. Niezwłoczne usuwanie defektów wykrytych tą metodą jest zazwyczaj dużo mniej kosztowne niż wprowadzanie poprawek do wdrożonego i eksploatowanego oprogramowania – szczególnie, jeśli weźmiemy pod uwagę również dodatkowe koszty wynikające z testowania potwierdzającego i testowania regresji.
Choć sam autor kodu powinien wykonywać testy jednostkowe, to szerszym testowaniem oprogramowania przeważnie zajmuje się tester. Dlaczego? Wynika to między innymi z odmiennego sposobu myślenia oraz cech tych osób. Programista nastawiony jest na zaprojektowanie i wykonanie produktu, zajmuje się budowaniem rozwiązań, a nie analizowaniem problemów. Dodatkowo testowanie własnej pracy może utrudniać mu efekt potwierdzenia, objawiający się oczekiwaniem, że stworzony przez niego kod będzie poprawny. Takie przeświadczenie często stanowi przeszkodę w uświadomieniu sobie popełnionych przez siebie błędów.
Tester zajmuje się weryfikacją i walidacją produktu, wyszukuje błędy i zgłasza je. Zgłoszenia, które powinny zawierać takie informacje jak opis awarii, działania, które do niej doprowadziły i oczekiwany rezultat tych działań, stają się potem podstawą pracy developerów. Z tego powodu istotne jest, by tester potrafił dokładnie zreferować problem i dbał o jakość tworzonych zgłoszeń.
Tego typu pracy sprzyja skrupulatność, kreatywność, krytyczne spojrzenie na swoje działania, ale także umiejętności interpersonalne. Ponieważ to zazwyczaj oni przekazują informacje o kłopotach związanych z produktem, testerzy powinni potrafić komunikować problemy w życzliwy i pozytywny sposób oraz wykazywać się asertywnością. Ważnym aspektem jest też niezależność testera, który ma inny punkt widzenia i inne uprzedzenia poznawcze w stosunku do autorów testowanego produktu. Dzięki temu może wykrywać inne rodzaje awarii oraz dokonywać obiektywnej oceny założeń przyjętych przez interesariuszy przy specyfikowaniu systemu.
Na testerach ciąży odpowiedzialność za zapewnienie dostatecznej jakości tworzonego oprogramowania, chociaż ich praca może być niekiedy postrzegana jako czynnik blokujący ukończenie projektu. Pamiętajmy jednak, by nie obarczać winą testerów za wszystkie awarie, które się zdarzają - w końcu gruntowne testowanie jest niemożliwe, a przekonanie o braku błędów jest błędem.
Bibliografia
Ariane 501 Inquiry Board, Ariane 5 Flight 501 Failure Report, Paryż, 19 lipca 1996
Błąd w przetwarzaniu czasu sparaliżował dziś PKP. I koleje w innych krajach też, 18 marca 2022 [1 sierpnia 2022]. Dostępne w Niebezpiecznik.pl:
[AKTUALIZACJA #3] Błąd w przetwarzaniu czasu sparaliżował dziś PKP. I koleje w innych krajach też
Stowarzyszenie Jakości Systemów Informatycznych, Certyfikowany tester Sylabus poziomu podstawowego ISTQB® Wersja 2018 V 3.1., 31 marca 2020
Stowarzyszenia Jakości Systemów Informatycznych, Słownik wyrażeń związanych z testowaniem wykorzystywanych w sylabusie CTFL 2018, 16 grudnia 2020