wersja mobilna
Online: 443 Sobota, 2017.11.18

Technika

Wprowadzenie do testowania oprogramowania embedded

czwartek, 19 października 2017 08:30

Zagadnienie testowania oprogramowania, zadomowione już dość dobrze w świadomości twórców aplikacji wysokopoziomowych, wciąż dopiero zdobywa popularność w środowisku programistów systemów wbudowanych. Zdecydowanie warto jednak bliżej przyjrzeć się tej tematyce - odpowiednio przeprowadzone testy pozwalają wykryć i wyeliminować znaczną większość błędów obecnych w oprogramowaniu. Wyższa jakość wytwarzanego oprogramowania przekłada się zaś na oszczędność czasu oraz pieniędzy, przede wszystkim zaś na dużo lepsze opinie jego użytkowników.

W tradycyjnym kaskadowym modelu wytwarzania oprogramowania testowanie stanowi jego ostatni, finalny etap przed wprowadzeniem produktu na rynek - zbyt dokładne stosowanie się do zaleceń tego modelu jest prawdopodobnie źródłem znacznej części kłopotów związanych z tym zagadnieniem. Bardzo często zdarza się bowiem, że prace nad projektem niespodziewanie się przedłużają, zaś termin jego ukończenia niebezpiecznie zbliża się ku końcowi. W takiej sytuacji nie ma już czasu na dokładne przetestowanie opracowanego kodu - w efekcie użytkownik otrzymuje nieefektywny program zawierający sporo błędów, z którego nie może być zadowolony.

Do dobrych praktyk z zakresu wytwarzania oprogramowania zalicza się inspekcję kodu. Szacuje się jednak, że w ten sposób wykryć można do 70% obecnych w kodzie błędów, zatem, jeśli chce się wytwarzać oprogramowanie wysokiej jakości, korzystanie z testów wydaje się koniecznością. O skuteczności takiego działania już dawno przekonali się przedstawiciele innych dziedzin inżynierii, w których (np. w budownictwie) przeprowadzanie testów opracowywanych produktów traktowane jest niezwykle poważnie, mimo związanych z tym często wysokich kosztów.

Cel testowania oprogramowania

Rys. 1. Uproszczony wykres pokazujący wzrost kosztu naprawy wykrytego problemu w funkcji stopnia zaawansowania projektu, na którym został on wykryty

Przed rozpoczęciem przygotowywania testów oprogramowania warto zadać sobie pytanie o główne cele tego działania. Dzięki temu możliwe jest precyzyjne określenie charakteru testów, jak również etapu realizacji projektu, na którym należałoby je rozpocząć. Wyodrębnić można cztery główne motywacje, które przyświecają testom oprogramowania:

  • znalezienie błędów w kodzie programu,
  • minimalizacja ryzyka ponoszonego przez użytkownika oraz twórcę programu,
  • ograniczenie kosztów utrzymania oraz dalszego rozwoju oprogramowania,
  • poprawa wydajności programu.

Wskazane cele są w wielu wypadkach ściśle ze sobą powiązane, zatem odpowiednio przeprowadzone testy pozwolą zazwyczaj osiągnąć widoczne efekty w każdym z wymienionych obszarów.

Jednym z popularnych argumentów przemawiających za przeprowadzaniem testów jest historia firmy HP, która w 1990 roku postanowiła oszacować wartość strat, jakie ponosi z powodu błędów w wytwarzanym przez siebie oprogramowaniu. Otrzymana kwota (400 mln dol.) okazała się stanowić trzecią część całego budżetu firmy przeznaczonego na badania i rozwój. Eliminacja strat wynikających z błędów w oprogramowaniu pozwoliłaby zwiększyć przychody przedsiębiorstwa o ponad 60%.

Jak pokazuje uproszczony wykres przedstawiony na rysunku 1, koszt eliminacji błędu oprogramowania jest tym mniejszy, im wcześniej błąd ten zostanie wykryty. Dużo taniej i szybciej naprawiać można błędy na etapie prototypu niż po wypuszczeniu produktu na rynek.

Testowanie pozwala również na maksymalizację wydajności i efektywności oprogramowania, m.in. poprzez wykrywanie i eliminację martwego i nieefektywnego kodu. Na dalszym etapie życia produktu może to pozwolić uniknąć konieczności wprowadzania kosztownych zmian sprzętowych.

Tragiczne skutki błędów w oprogramowaniu

Prawdopodobnie jednym z najbardziej tragicznych przykładów tego, jak duże szkody przynieść mogą błędy oprogramowania systemów wbudowanych, jest przypadek maszyny do radioterapii o nazwie Th erac-25. Sprzęt ten wykorzystywany był do leczenia pacjentów w latach 80. Na skutek błędnego działania oprogramowania przynajmniej pięciu pacjentów otrzymało zbyt duże dawki promieniowania, co było bezpośrednią przyczyną ich śmierci. Na etapie badania przyczyn wypadku okazało się, że producent w celi ograniczenia kosztów zrezygnował z implementacji w tym produkcie niektórych mechanizmów zabezpieczeń stosowanych we wcześniejszych modelach.

Rodzaje testów i moment ich rozpoczęcia

Rys. 2. Klasyczny kaskadowy model wytwarzania oprogramowania

W celu minimalizacji kosztów usuwania wykrytych błędów proces testowania powinien zostać uruchomiony na jak najwcześniejszym etapie życia urządzenia. Pierwszymi wykonywanymi testami powinny być zatem testy jednostkowe, sprawdzające indywidualnie poprawność pracy każdego z modułów systemu.

Testy jednostkowe (Unit Test) polegają na testowaniu poprawności działania pojedynczych elementów systemu, bez uwzględnienia współpracy z pozostałymi modułami oraz układami. Wszystkie interakcje zewnętrzne (jak np. połączenia z innymi modułami) zastępowane są programowo przez tzw. zaślepki (stubs). Podczas tego typu testów sprawdza się zazwyczaj kod pod kątem poprawnej reakcji na wybrane wartości zmiennych wejściowych - od wartości średnich z dozwolonego zakresu, poprzez wartości graniczne, aż do tych wykraczających poza dozwolony zakres, sprawdzając m.in. prawidłowość obsługi sytuacji wyjątkowych.

Testy integracyjne wykonuje się w celu wykrycia błędów w interfejsach i interakcjach pomiędzy modułami. Sprawdzeniu podlega głównie komunikacja pomiędzy poszczególnymi elementami systemu. Testowanie może przebiegać w jednym z dwóch kierunków, tj. od góry do dołu (jako pierwsze testowane są moduły stojące na szczycie organizacyjnej hierarchii systemu) lub od dołu do góry (sytuacja odwrotna).

Testy systemowe sprawdzają, czy cały system (urządzenie) spełnia wymagania zawarte w specyfikacji. Weryfikacji podlegają również wymagania niefunkcjonalne, takie jak wydajność, niezawodność oraz użyteczność. Testowaniu podlega całe urządzenie, tego typu testy można zatem rozpocząć dopiero po opracowaniu pełnej wersji oprogramowania.

W przypadku dokonywania jakichkolwiek późniejszych modyfikacji kodu programu dobrą praktyką jest przeprowadzanie testów regresyjnych. Tego typu testy są zazwyczaj zautomatyzowane, mają zdefiniowaną listę przypadków testowych oraz poprawnych danych wyjściowych. Ich przeprowadzanie jest bardzo ważne - pozwalają wykrywać niepożądane negatywne skutki zmian wprowadzanych w oprogramowaniu.

Kryterium zakończenia testów

Testowanie jest procesem, który w zasadzie nigdy może nie mieć końca - trudno obiektywnie stwierdzić, że program na pewno nie zawiera już żadnych błędów albo że napisany jest w sposób możliwie najbardziej optymalny i efektywny. Z tego też powodu ważne jest przyjęcie już przed rozpoczęciem tego procesu określonych kryteriów, które pozwolą precyzyjnie określić moment jego zakończenia.

W przypadku tworzenia oprogramowania o znaczeniu krytycznym (np. dla przemysłu lotniczego) kryterium zakończenia testów często narzucone jest odgórnie, w różnego typu dokumentach standaryzacyjnych i normalizacyjnych. Przykładowo, w przemyśle lotniczym wymagania takie określa specyfikacja DO-178B wydana przez Federalną Administrację Lotnictwa (FAA).

Jeśli istnieje jednak potrzeba samodzielnego zdefiniowania kryterium zakończenia testów, najczęściej rozważa się następujące warunki:

  • liczba błędów znaleziona w ostatniej iteracji jest mniejsza niż przyjęty próg,
  • podczas obecnej iteracji nie znaleziono żadnego błędu przy jednoczesnym osiągnięciu określonego poziomu pokrycia kodu.

Należy mieć na uwadze, że nie jest możliwe stwierdzenie z całkowitą pewnością, że program nie zawiera już żadnych błędów. Nasuwa się zatem pytanie, jaką maksymalną liczbę błędów można uznać za dopuszczalną przed przekazaniem produktu odbiorcy. Nie ma uniwersalnych reguł, które pozwalałyby odpowiedzieć na tę kwestię.

Przykładowo, bardzo rzadki błąd o trudnej do zdefiniowania przyczynie, występujący co kilkadziesiąt godzin testów, w niektórych rodzajach aplikacji będzie mógł zostać potraktowany jako dopuszczalny, w innych zaś priorytetem będzie jego wyeliminowanie. Poszukując optymalnego rozwiązania tego problemu, należałoby zapewne uwzględnić przeznaczenie wytwarzanego oprogramowania oraz inne czynniki, jak np. ramy czasowe projektu.

Dobór przypadków testowych

Chcąc przetestować działanie programu w sposób doskonały, należałoby przynajmniej jednokrotnie sprawdzić każdą możliwą kombinację danych wejściowych oraz ścieżki decyzyjnej. W rzeczywistym świecie takie podejście jest niestety niezbyt praktyczne ze względu na swoją czasochłonność - nawet dla dość prostych programów wymagałoby testów trwających latami.

W praktyce głównym zadaniem osoby testującej jest taki dobór testów, który umożliwi uzyskanie jak najwyższego prawdopodobieństwa ujawnienia błędów w rozsądnym przedziale czasowym. By to osiągnąć, często stosuje się kombinację testów funkcjonalnych oraz testów uwzględniających poziom pokrycia kodu programu.

Testowanie czarnoskrzynkowe

Przy podejściu funkcjonalnym testowany program określa się często jako czarną skrzynkę - przypadki testowe dobiera się pod kątem prawidłowego, zgodnego ze specyfikacją działania aplikacji, bez konieczności uwzględniania jej struktury i "zaglądania" do kodu źródłowego.

Testy funkcjonalne koncentrują się przede wszystkim na sprawdzeniu poprawności relacji między danymi wejściowymi i wyjściowymi, pomijając szczegóły działania przekształcającego je algorytmu. Poniżej przedstawiono przykłady tego typu testów:

  • testy przeciążeniowe - celowe nadmierne obciążanie kanałów wejściowych pozwala sprawdzić zachowanie systemu w trudnych warunkach, np. przy zwiększonej liczbie pracujących użytkowników i/lub liczbie zdarzeń wymagających obsłużenia. Pozwala poznać granice efektywnego działania urządzenia.
  • testy wartości granicznych - sprawdzają zachowanie się systemu w przypadku podania danych wejściowych o dozwolonych wartościach granicznych (np. największa i najmniejsza dozwolona liczba całkowita dodatkowo zmniejszona/zwiększona o jeden), a także danych, które spowodują wygenerowanie granicznych dozwolonych wartości wyjściowych.
  • testy sytuacji wyjątkowych - powinny powodować wywołanie trybu awaryjnego lub innego sposobu obsługi sytuacji wyjątkowych.
  • technika zgadywania błędów (error guessing) - tester, bazując na wcześniejszym doświadczeniu, próbuje przewidzieć, jakie błędy mogą być obecne w testowanym oprogramowaniu. Projektuje testy w taki sposób, by te ewentualne błędy ujawnić.
  • testy losowe - przypadki testowe dobierane są na podstawie algorytmu pseudolosowego. Pomocne przy testowaniu niezawodności oraz wydajności systemu.
  • testy małpie (monkey testing) - polegają na losowym wyborze danych wejściowych, bez uwzględnienia prawidłowego sposobu obsługi urządzenia. Wykorzystywane szczególnie w przypadku testowania interfejsów użytkownika poprzez losowe naciskanie i ustawianie różnych kontrolek i przycisków.
  • testy wydajności - jeden z rodzajów czarnoskrzynkowych testów niefunkcjonalnych, mający na celu testowanie wydajności i efektywności oprogramowania.

Testy czarnoskrzynkowe konstruuje się w oparciu o specyfikację oprogramowania, mogą one zostać zaprojektowane bezpośrednio po zdefiniowaniu wymagań produktu. Dzięki temu mogą być gotowe już w momencie zakończenia prac nad oprogramowaniem - pozwala to niewątpliwie skrócić czas testowania.

Testowanie strukturalne (biała skrzynka)

Jedną z głównych słabości testowania funkcjonalnego jest to, że zazwyczaj nie sprawdza ono całości kodu źródłowego. Jego przeciwieństwem jest testowanie strukturalne, skupiające się na wykonaniu i sprawdzeniu wszystkich (w idealnym przypadku) wyrażeń kodu źródłowego.

Testy strukturalne, znane również jako testy białej lub szklanej skrzynki, projektowane są z pełną znajomością kodu źródłowego aplikacji. Z tego też powodu mogą zostać opracowane dopiero po zakończeniu prac nad oprogramowaniem.

Z punktu widzenia systemów wbudowanych, testy strukturalne są bardzo ważne, ponieważ pokazują stopień przetestowania kodu i pozwalają dzięki temu oszacować liczbę niewykrytych jeszcze błędów, z którymi będzie się należało zmierzyć w przyszłej fazie życia produktu. Przykładowe testy strukturalne to:

  • pokrycie instrukcji kodu - przypadki testowe dobrane w taki sposób, by powodowały przynajmniej jednokrotne wykonanie każdej instrukcji kodu źródłowego.
  • pokrycie decyzji - dobór przypadków testowych w taki sposób, by powodowały przynajmniej jednokrotne przejście każdej ścieżki decyzji w programie.
  • pokrycie warunków - dobór przypadków testowych tak, by powodowały przynajmniej jednokrotne wykonanie każdego wyrażenia warunkowego dla wszystkich wartości logicznych.

Podczas tego typu testów przydatne jest korzystanie z różnych narzędzi pozwalających wymuszać określone stany pamięci oraz wykonywanie określonych instrukcji (jak np. interfejs JTAG).

Testy szarej skrzynki

Testy białoskrzynkowe związane są ściśle ze strukturą kodu, przez co są znacznie trudniejsze i droższe w utrzymaniu niż testy czarnoskrzynkowe. O ile testy czarnoskrzynkowe pozostają prawidłowe tak długo, nim zmianie nie ulegnie ogólna specyfikacja programu (relacje między danymi wejściowymi a wyjściowymi), konieczność zmiany testów białoskrzynkowych może zachodzić przy każdej ingerencji w kod źródłowy. Najbardziej efektywne są zatem takie testy, które wykorzystują ogólną znajomość kodu źródłowego, bez ścisłego przywiązania do jego szczegółów.

Tego typu testy określane są czasem jako testy szarej skrzynki. Mogą być one bardzo efektywne w połączeniu z techniką zgadywania błędów - pozwalają bardzo dokładnie sprawdzić najbardziej "podejrzane" fragmenty kodu. Mogą być również użyteczne w przypadku integracji nowego kodu z już istniejącym, przetestowanym i stabilnie działającym programem.

Unikatowy charakter testowania oprogramowania wbudowanego

Testowanie oprogramowania dla systemów wbudowanych jest w wielu kwestiach bardzo zbliżone do testowania aplikacji komputerowych. Istnieją jednak pewne zasadnicze różnice. Programiści systemów embedded mają często bezpośredni dostęp do różnego typu narzędzi sprzętowych wspierających proces testowania, co zazwyczaj nie jest możliwe w przypadku testowania aplikacji.

Ponadto systemy wbudowane charakteryzują się zazwyczaj pewnymi właściwościami, które powinny zostać uwzględnione w planie testów. Niezwykle szeroki jest również zakres realizowanych przez nie zadań, zaś ewentualne błędy w działaniu mogą mieć często tragiczne konsekwencje. Poniżej przedstawiono główne cechy odróżniające oprogramowanie wbudowane od aplikacji komputerowych:

  • oprogramowanie wbudowane musi działać niezawodnie przez długi okres.
  • oprogramowanie wbudowane wykorzystywane jest często w zastosowaniach krytycznych, takich jak np. ochrona życia ludzkiego.
  • systemy wbudowane mają często tak duże znaczenie (oraz koszt), że przewidziany dla oprogramowania margines błędu oraz efektywności pracy jest niewielki lub żaden.
  • oprogramowanie wbudowane często musi wykrywać oraz naprawiać potencjalne problemy sprzętowe.

Z powodu wspomnianych różnic testowanie oprogramowania embedded różni się od testowania aplikacji komputerowych w niżej opisanych kwestiach:

  • podczas testowania oprogramowania wbudowanego kładzie się zazwyczaj większy nacisk na sprawdzanie działania w czasie rzeczywistym. Obsługa przerwań czy przetwarzanie równoległe to zagadnienia, z którymi często wiąże się sporo problemów, muszą zatem zostać dogłębnie przebadane.
  • systemy wbudowane charakteryzują się zazwyczaj ograniczonym poziomem zasobów, wymagane jest zatem przeprowadzenie dokładniejszych testów wydajnościowych.
  • podczas testowania systemów wbudowanych często możliwe jest korzystanie z dodatkowych narzędzi mierzących jakość testów.
  • bardzo często od oprogramowania wbudowanego wymaga się większego poziomu niezawodności niż od aplikacji komputerowych.
Błędy związane z pracą w czasie rzeczywistym

Rys. 3. Schematyczny przebieg testowania czarnoskrzynkowego

Systemy wbudowane często muszą radzić sobie z jednoczesną obsługą dużej liczby asynchronicznych zdarzeń, zatem podczas projektowania i doboru testów należy szczególną uwagę poświęcić pracy systemu w czasie rzeczywistym i możliwym problemom z tym związanym. Zestaw testów powinien zawierać przynajmniej typowe oraz najgorsze tego typu sytuacje.

Należy sprawdzić reakcję systemu na różne sekwencje zdarzeń. Przykładowo, testując samochodowy komputer pokładowy, warto sprawdzić, co stanie się przy jednoczesnym uruchomieniu ogrzewania, wycieraczek oraz świateł albo po szybkim kilkunastokrotnym włączeniu i wyłączeniu radioodbiornika.

W każdym systemie czasu rzeczywistego niektóre kombinacje zdarzeń (określane czasem jako sekwencje krytyczne) mogą wywołać wzrost czasu opóźnienia pomiędzy wystąpieniem zdarzenia a wygenerowaniem na nie odpowiedzi. Wszystkie takie sekwencje powinny zostać włączone do zestawu testów.

W przypadku niektórych zadań wykonywanych przez system wbudowany pojęcie deadline’u czasowego może mieć większe znaczenie niż wzrost opóźnienia czasu reakcji na zdarzenie. Czasem pewne czynności muszą być realizowane w dokładnie określonym terminie, a żadne odstępstwa od tej zasady nie mogą zostać zaakceptowane. Dla takich sytuacji należy szczególnie dokładnie zbadać możliwość wystąpienia sekwencji krytycznych.

Stopień uzależnienia poprawności pracy urządzenia od spełnienia określonych reżimów czasowych w realizacji zadań jest jednym z kryteriów klasyfikacji systemów wbudowanych. W systemie o ostrych ograniczeniach czasowych (ang. hard real-time) przekroczenie zakładanego terminu (bez względu na wielkość tego przekroczenia) powoduje katastrofalne skutki, takie jak zagrożenie życia lub zdrowia użytkownika albo uszkodzenie lub całkowite zniszczenie urządzenia.

Jeśli przekroczenie terminu wiąże się z mniej poważnymi skutkami negatywnymi, których znaczenie zależy w dodatku od wielkości przekroczenia (np. pogorszenie wydajności), system klasyfikuje się jako o łagodnych ograniczeniach czasowych (soft real-time).

Kolejną grupę błędów stanowią przypadki, w których system zmuszony jest do pracy w warunkach maksymalnego obciążenia. Mogą objawić się wtedy problemy, których nie da się zaobserwować w innych okolicznościach - np. kłopoty z dynamiczną alokacją pamięci lub przepełnienie kolejki zadań oczekujących na obsłużenie.

Zazwyczaj detekcja błędów związanych z pracą w czasie rzeczywistym możliwa jest dopiero po podłączeniu systemu do realnego środowiska pracy lub jego symulatora. Skorzystanie z symulatora możliwe jest jednak tylko w wybranych przypadkach - często jego opracowanie w ogóle nie wchodzi w grę, ponieważ znacznie przekraczałoby zakładany budżet projektu. Jeśli jednak przy projektowaniu korzysta się z narzędzi opartych na językach opisu sprzętu, takich jak VHDL czy Verilog (np. stosując układy FPGA lub ASIC), można spróbować użyć tych samych narzędzi do opracowania środowiska testowego.

Pomiar stopnia pokrycia kodu przez testy

Rys. 4. Schematyczny przebieg testowania białoskrzynkowego

By móc poprawnie oszacować skuteczność procesu testowania oraz prawidłowo ustalić właściwy moment jego zakończenia, konieczne jest posiadanie zdolności wiarygodnego pomiaru stopnia pokrycia kodu przez przeprowadzone testy. Niektóre z nich przedstawione zostaną w dalszej części tekstu.

Pomiaru pokrycia kodu opiera się zazwyczaj na umieszczonych w programie dodatkowych instrukcjach. Jednym z takich sposobów jest umieszczanie instrukcji informującej o wykonaniu danego miejsca programu na początku każdego fragmentu kodu określanego jako blok podstawowy.

Blok podstawowy to w tym znaczeniu zestaw sekwencyjnie wykonywanych instrukcji mających jeden punkt wejściowy. Koniec bloku stanowi punkt decyzyjny lub wyrażenie kontrolne, takie jak return lub goto. Innymi słowy, wykonanie dowolnej instrukcji bloku podstawowego informuje o wykonaniu wszystkich instrukcji wchodzących w jego skład.

Prostą, a jednocześnie niezwykle skuteczną metodą może być zatem umieszczenie na początku każdego bloku podstawowego instrukcji typu printf(). Jest to niestety metoda dość inwazyjna, znacznie spowalniająca działanie programu. Jeśli w projekcie korzysta się z systemu operacyjnego, alternatywą może być wykorzystanie możliwości logowania zdarzeń oferowanych przez ten system. Funkcję typu printf() zastąpić można również pojedynczą instrukcją zapisu określonych adresów pamięci. Po zakończeniu testów dodatkowe oprogramowanie może odczytać stan pamięci i na tej podstawie stworzyć mapę pokrycia kodu programu testami.

Programowy pomiar pokrycia kodu jest dość łatwy w implementacji, jednak jego główną wadą jest wysoki stopień ingerencji w pracę programu. Nie tylko spowalnia jego działanie, ale wpływa również na zwiększenie rozmiaru kodu. Może okazać się to dużym kłopotem, szczególnie jeśli system dysponuje ograniczonymi zasobami pamięci ROM.

Nie tylko pokrycie instrukcji kodu

Pomiar pokrycia instrukcji kodu nie jest jedynym dostępnym sposobem badania skuteczności testów. Do kontroli efektywności testów można wykorzystać inne wskaźniki, uznawane za bardziej wiarygodne, takie jak pokrycie decyzji (DC, Decision Coverage) oraz zmodyfikowane pokrycie warunków decyzji (MCDC, Modified Condition/Decision Coverage).

Pomiar pokrycia decyzji sprawdza odsetek możliwych wyników decyzji, które zostały przetestowane przez zestaw testowy. Zmodyfikowane pokrycie warunków decyzji informuje, jaki procent pokrycia wyników pojedynczych warunków niezależnie wpływających na wynik decyzji został wykonany przez zestaw przypadków testowych. Uzyskanie pełnego zmodyfikowanego pokrycia warunków decyzji (100%) jest jednoznaczne z uzyskaniem 100% pokrycia warunków decyzji oraz 100% pokrycia instrukcji kodu.

Wskaźniki DC oraz MCDC pozwalają wykrywać błędy logiczne w kodzie skuteczniej niż w przypadku pokrycia instrukcji. Przykładem może być następujące wyrażenie:

if (wyrażenie logiczne)
{
< instrukcje w bloku warunkowym >;
}
< dalsze instrukcje - brak bloku else if >

Powyższy kod uzyska 100% pokrycia instrukcji, jeśli podczas testów wyrażenie logiczne przyjmie wartość True. Nie ma jednak pewności, że przetestowane zostało działanie programu dla wyrażenia logicznego o wartości False, nie ma również możliwości pozyskania tej informacji. Do uzyskania 100% pokrycia decyzji konieczne byłoby zaś sprawdzenie obu wartości wyrażenia logicznego.

Zmodyfikowane pokrycie warunków decyzji pozwala jeszcze bardziej szczegółowo zmierzyć efektywność testów, w szczególności zaś wykonanie pojedynczych warunków logicznych wchodzących w skład bardziej złożonych wyrażeń.

if (A | | B)
{
< instrukcje w bloku warunkowym >;
}

By w powyższym przykładzie uzyskać 100% pokrycia decyzji, należałoby wykonać dany kod przynajmniej dwukrotnie - dla wyrażenia o wartości całkowitej True oraz False, bez uwzględniania wartości pojedynczych warunków. Pomiar MCDC dodatkowo sprawdza również wykonanie warunków A oraz B z osobna - pozwala zatem uzyskać pewność, że przetestowano działanie programu dla wszystkich stanów pojedynczych warunków logicznych. W przypadku wykrycia błędu ułatwia też znalezienie jego przyczyny, ponieważ informuje nie tylko o wartości całego wyrażenia, dla której wystąpił błąd, ale również o wartościach elementów składowych.

Testowanie wydajności

Testowanie wydajności powinno być ostatnim etapem testowania systemu, pozwalającym na optymalizację jego funkcjonowania. W przypadku systemów wbudowanych jest to element szczególnie ważny, ponieważ może uchronić przed potrzebą przeprojektowania zasobów sprzętowych. Przykładowo, znalezienie i eliminacja martwego kodu pozwoli uniknąć konieczności doposażenia układu w dodatkową pamięć, zaś wyeliminowanie zbędnych opóźnień zapobiegnie kosztom związanym z wymianą procesora na szybszy.

Testowanie wydajności opiera się głównie na pomiarze czasu wykonywania pojedynczych funkcji. Czas ten zależny jest od wielu czynników (m.in. obciążenie systemu operacyjnego, obsługa przerwań, zawartość pamięci podręcznej procesora w trakcie wywoływania funkcji), należy więc traktować jego wartość jako niedeterministyczną. Do pomiarów używa się zatem narzędzi statystycznych - w efekcie uzyskuje się minimalny, maksymalny oraz średni czas wykonywania każdej z funkcji. Analiza tych wartości pozwala znaleźć słabe punkty programu.

Bardzo przydatnym testem jest również analiza użycia pamięci dynamicznej - pozwala wykryć m.in. wycieki pamięci. Często możliwie jest dzięki temu zlokalizowanie przyczyn bardzo rzadko występujących i trudno powtarzalnych usterek, które mogą powodować np. okazjonalne zawieszanie pracy urządzenia.

Podsumowanie

Testowanie jest zazwyczaj ostatnim etapem cyklu projektowego, co znacznie utrudnia jego prowadzenie. Lepszym rozwiązaniem wydaje się wcześniejsze rozpoczęcie procesu testowania, mimo że nie wszystkie rodzaje testów mogą być przeprowadzane przed uzyskaniem kompletnego produktu. Należy jednak dążyć do tego, by przeprowadzać testy tak szybko, jak tylko jest to możliwe - przede wszystkim dlatego, że zmniejsza to koszt naprawy wykrytych usterek.

Warto mieć również świadomość, że testowalność, czyli zdolność do sprawdzenia poprawności działania oprogramowania po wprowadzeniu zmian, powinna być jednym z kluczowych kryteriów każdego projektu, branym pod uwagę już od pierwszych etapów projektowania.

Jeden z twórców inżynierii oprogramowania, Tom DeMarco, stwierdził, że nie można kontrolować tego, czego nie da się zmierzyć. Jeśli chce się zatem kontrolować jakość wytwarzanego oprogramowania, należy opanować narzędzia pozwalające tę jakość oceniać, m.in. poprzez przemyślane i efektywne testowanie.

Damian Tomaszewski