Testowanie oprogramowania
Źródło: Krzysztof Sacha „Inżynieria programowania” PWN W-wa 2010
Testowanie jest procesem eksperymentalnego badania programu lub jego
komponentu, polegającym na próbnym wykonywaniu w znanych warunkach,
rejestrowaniu wyników i ocenianiu na ich podstawie właściwości tego programu lub
komponentu.
1. Poziomy testowania
Testowanie jednostkowe
Testowanie integracyjne
Testowanie systemowe
testy funkcjonalne (functional testing),
testy wydajnościowe (performance testing),
testy przeciążeniowe (stress testing),
testy bezpieczeństwa (security testing),
testy odporności (recovery testing),
testy zgodności (compatibility testing),
testy dokumentacji (documentation testing),
Testowanie akceptacyjne
2. Organizacja procesu testowania
2.1. Planowanie testów
2.2. Przygotowanie testów
2.3. Testowanie a usuwanie defektów
Po wykonaniu serii testów i wprowadzeniu poprawek powstaje kolejna
wersja programu, która jest poddawana podwójnemu sprawdzeniu:
czy wszystkie dotychczas działające funkcje programu działają
nadal (testy regresji),
czy wszystkie wykryte błędy zostały rzeczywiście usunięte
(retesty).
3. Metryki Testowania
Aby móc efektywnie planować i zarządzać wykonaniem procesu testowania
oprogramowania, potrzebny jest jakiś sposób pomiaru jakości testów oraz sposób
pomiaru postępu prac w czasie trwania procesu testowania. Narzędziem pomiaru są
metryki testowania, czyli ilościowe miary odzwierciedlające różne cechy
zaprojektowanych, lub już wykonanych, testów.
analiza pokrycia kodu (code coverage analysis).
metryki pokrycia kodu (code coverage metrics),
analizy stopnia pokrycia wymagań testami funkcjonalnymi.
3.1. Metryki pokrycia kodu
Pokrycie bloków instrukcji
Pokrycie decyzji (decision coverage)
Pokrycie ścieżek
3.2. Metryki pokrycia wymagań
Pokrycie wymagań (requirements coverage)
Pokrycie błędów
Testowanie przypadków użycia:
pokrycia biznesowych przypadków użycia zestawami testów,
pokrycia systemowych przypadków użycia scenariuszami przypadków
testowych,
pokrycia scenariuszy systemowych przypadków użycia przez przypadki
testowe.
3.3. Inne metryki
Liczba wykrytych defektów
Częstość defektów
Procent defektów poprawionych
Szacowanie liczby wszystkich defektów
4. Metody testowania
Celem testowania jest doświadczalne porównanie wyników działania programu ze
wzorcem, którym jest specyfikacja tego programu. Jeśli wyniki są zgodne, to program
uznaje się za poprawny.
4.1 Testowanie czarnej skrzynki
Analiza klas równoważności
Analiza wartości brzegowych (boundary value analysis)
Testowanie sposobów użycia (use case testing)
Testowanie losowe (Monte Carlo method)
4.2. Testowanie białej skrzynki
Testowanie ścieżek (path testing) - Można w tym celu wykorzystać pułapki
programu uruchomieniowego (debugger).
Testowanie mutacyjne (mutation testing)
5. Automatyzacja testowania
Narzędzia elementarne - sterowniki i namiastki
Automatyzacja testów jednostkowych biblioteki Junit Nunit
Narzędzia pokrycia kodu (code coverage tools),
Odtwarzanie działań użytkownika (record and playback feature)
Testowanie wydajności
Testowanie jest procesem eksperymentalnego badania programu lub jego
komponentu, polegającym na próbnym wykonywaniu w znanych warunkach,
rejestrowaniu wyników i ocenianiu na ich podstawie właściwości tego programu lub
komponentu. Przedmiotem oceny może być poprawność wytwarzanych przez
program wyników, wydajność lub szybkość obliczeń albo inne właściwości określone
w specyfikacji wymagań.
Aby uzyskać gwarancję prawidłowego zachowania w każdej sytuacji, należałoby
sprawdzić działanie programu dla wszystkich możliwych wartości danych
wejściowych. Astronomiczna liczba możliwych wartości danych, które w dodatku
mogą być wprowadzane w różnej kolejności i w różnym czasie, sprawia, że takie
pełne sprawdzenie programu jest niemożliwe do wykonania. Dlatego w praktyce
zastępuje się pełne sprawdzenie programu pewną liczbą eksperymentów
polegających na wprowadzeniu wybranych wartości danych wejściowych i
porównaniu otrzymanych wyników z oczekiwaniami. Każdy eksperyment, który
można opisać w kategoriach wartości wprowadzanych danych, akcji wykonywanej
przez użytkownika i oczekiwanych wyników, jest nazywany przypadkiem testowym
(test case).
Liczba przypadków testowych oraz sposób doboru wartości danych mają
decydujący wpływ na jakość i wiarygodność procesu testowania. Dlatego ważną
czynnością poprzedzającą etap testowania jest projektowanie testów, mające na celu
określenie takiego zestawu przypadków testowych, który umożliwi efektywne
sprawdzenie wszystkich aspektów poprawności programu w ograniczonym czasie.
Wykonanie pewnego zestawu testów nie jest czynnością jednorazową. Każda
zmiana już przetestowanego fragmentu programu, związana z poprawianiem
ujawnionych błędów lub z ulepszaniem jego funkcji, może wprowadzić nowe błędy lub
doprowadzić do ujawnienia się błędów istniejących od dawna, lecz niewidocznych w
poprzedniej wersji programu. Dlatego dobrą praktyką jest powtarzanie wszystkich już
wykonanych testów po każdej zmianie poddanego testom programu. Takie
powtarzane testy, których celem jest upewnienie się, że pomimo dokonanych zmian
program nadal działa, są nazywane testami regresji. Zaniechanie testów regresji
może doprowadzić do pozostawienia błędów w działaniu programu.
Testowanie oprogramowania jest złożonym procesem, pochłaniającym wiele
czasu i zasobów oraz wymagającym starannego przygotowania. Dlatego różne
działania związane z testowaniem występują w różnych fazach procesu wytwórczego.
Podstawowe działania obejmują przygotowanie planu testów, zaprojektowanie
przypadków testowych, przygotowanie środowiska testowego, testowanie, ocenę
wyników i usunięcie błędów w działaniu programu.
1. Poziomy testowania
Podstawowym celem testowania jest znalezienie i usunięcie błędów w działaniu
programu. Każdy błąd (error), czyli objaw błędnego działania programu ujawniony
podczas testów, świadczy o jakimś defekcie (fault) znajdującym się w kodzie
testowanego programu. Znalezienie tego defektu na podstawie zaobserwowanych
objawów błędu jest tym trudniejsze, im dłuższa droga dzieli miejsce defektu od
miejsca ujawnienia się błędu. Aby skrócić tę drogę i ułatwić lokalizowanie defektów,
można rozpocząć testowanie jeszcze przed integracją całości oprogramowania i
testować najpierw poszczególne jednostki programu, potem ich współdziałanie, a na
końcu - działanie całego systemu. Przyjęcie takiej organizacji tworzy kilka poziomów
testowania, przeznaczonych na ocenę różnych właściwości i różnych aspektów
działania oprogramowania.
Rysunek 1. Model V procesu testowania oprogramowania
Model procesu testowania, pokazany na rys. 1, przedstawia kolejność (linie
ciągłe) oraz związki (linie przerywane) poziomów testowania z etapami lub
działaniami procesu rozwoju oprogramowania. Dobrą praktyką inżynierską jest
projektowanie zestawu testów w tej fazie rozwoju, w której są rozważane te aspekty
oprogramowania, które będą podlegać testowaniu. Praktyka dowodzi, że
opracowanie koncepcji testowania sprzyja lepszemu zrozumieniu i większej
jednoznaczności sformułowania wymagań.
Testowanie jednostkowe
Przedmiotem testowania jednostkowego (unit testing, module testing) są
podstawowe jednostki programu opisane w projekcie szczegółowym. Postać tych
jednostek zależy od technologii implementacji - mogą być nimi podprogramy
(procedury, funkcje) napisane w języku strukturalnym, skrypty SQL albo metody lub
klasy zapisane w języku obiektowym. Celem testowania na tym poziomie jest
sprawdzenie zgodności działania wszystkich opracowanych jednostek z ich
specyfikacją, wynikającą z projektu, oraz wykrycie i usunięcie jak największej liczby
błędów.
Szczegółowy projekt programu powstaje w procesie kaskadowym w końcowej
części fazy projektowania. Natomiast w procesie iteracyjnym projekt taki powstaje w
każdej iteracji fazy konstrukcji i dotyczy tej części oprogramowania, która jest
budowana w bieżącej iteracji. W tym też czasie powinny zostać zaplanowane testy
jednostkowe. Sposób realizacji testowania jednostek zależy od wymaganej
niezawodności programu. W tych zastosowaniach, w których wymagania są wysokie,
testowanie jednostkowe tworzy odrębny etap procesu wytwórczego, wykonywany
przez niezależny zespół testujący. W pozostałych przypadkach testowanie
jednostkowe realizują na ogół deweloperzy, którzy opracowali testowane programy, a
czynności testowania nakładają się na okres implementacji programów.
Problemem tego poziomu testowania jest zależność testowanej jednostki od
innych jednostek programu, które np. wywołują testowany podprogram albo są przez
niego wywoływane. Przeprowadzenie testów wymaga w takim przypadku użycia
specjalnego oprogramowania wspomagającego albo opracowania sterowników
testowych, wywołujących badaną jednostkę z odpowiednimi argumentami, oraz
namiastek, symulujących działanie brakujących jednostek wywoływanych.
Przygotowane w ten sposób środowisko testowania powinno być zachowane i
wykorzystane do testów regresji - po poprawkach związanych z usuwaniem błędów
oraz później, po zmianach wprowadzanych w kolejnych wersjach programu.
Testowanie integracyjne
Połączenie dwóch współdziałających jednostek tworzy nową jednostkę, w której
ujawniają się błędy związane z niedopasowaniem mechanizmów ich współpracy.
Przedmiotem działań podejmowanych podczas testowania integracyjnego (interface
testing) jest łączenie jednostek programu w coraz większe komponenty i sprawdzanie
zgodności ich działania ze specyfikacją wynikającą z projektu architektury
oprogramowania. Celem testów jest sprawdzenie funkcjonowania interfejsów -
sposobu wywoływania usług i przekazywania argumentów, postaci zwracanych
rezultatów, zgodności jednostek miary używanych we współpracujących jednostkach,
zgodności wyjątków zgłaszanych w jednej, a obsługiwanych w innej jednostce itp.
Wszystkie wykryte błędy są usuwane, a cały proces trwa aż do zintegrowania i
przetestowania działania całości oprogramowania.
Projekt architektury oprogramowania powstaje w procesie kaskadowym w
początkowej części fazy projektowania. W procesie iteracyjnym projekt taki powstaje
w fazie opracowania i podlega uszczegółowieniu podczas konstrukcji
oprogramowania. Wraz z opracowywaniem architektury powinien też powstać plan
testowania integracyjnego. Wykonanie czynności integracji i testowania programu
tworzy na ogół osobny etap działań, wykonywany po zakończeniu implementacji:
całości - w procesie kaskadowym, lub części - w procesie iteracyjnym. Testowanie
mogą przeprowadzać deweloperzy lub wyspecjalizowany zespół testujący.
Proces integracji programu może przebiegać w różny sposób. W metodzie
„wielkiego wybuchu" zakłada się złożenie wszystkich jednostek oprogramowania i
testowania od razu całości. Mankamentem tego podejścia jest trudność lokalizowania
ujawnianych błędów. Doświadczenie pokazuje, że nakładające się na siebie
wielokrotne błędy potrafią maskować swoje istnienie, sumować się w dziwny sposób
lub wykazywać objawy sugerujące błędy w zupełnie innych częściach programu.
Dlatego częściej stosowana - zwłaszcza w dużych projektach - jest metoda
stopniowej integracji i testowania coraz większych grup jednostek, prowadząca na
końcu do zbudowania całości systemu.
Stopniową integrację programu można prowadzić w sposób
wstępujący lub
zstępujący.
Strategia wstępująca zaczyna od łączenia jednostek najniższego poziomu w
większe komponenty, te w jeszcze większe aż do osiągnięcia poziomu całego
programu. Zaletą takiego podejścia jest zredukowanie liczby koniecznych do
opracowania namiastek - w każdym kroku zarówno testowany komponent, jak i
wszystkie wywoływane przez niego podprogramy są już dostępne. Wadą jest brak
dostępności interfejsu użytkownika, ulokowanego często w głównej części programu,
w początkowych krokach integracji. Strategia zstępująca przebiega w odwrotnym
kierunku. Do programu głównego dołącza się podprogramy wywoływane, aż do
poziomu najprostszych jednostek. Ta strategia wymaga opracowania większej liczby
namiastek, ale w zamian zapewnia dostęp do interfejsu użytkownika od samego
początku.
W większości praktycznych przypadków testowanie integracyjne przeprowadza
się stopniowo, łącząc elementy strategii zstępującej i wstępującej. Strategię
jednoczesnej integracji całości stosuje się bardzo rzadko.
Testowanie systemowe
Przedmiotem testowania systemowego (system testing) jest całość
oprogramowania zintegrowana i zainstalowana w odpowiednim środowisku
wykonawczym. Celem testów jest sprawdzenie zgodności sposobu działania
wszystkich funkcji oprogramowania ze specyfikacją oraz weryfikacja innych
właściwości systemu określonych przez wymagania niefunkcjonalne. Wiarygodność
wyników testów wymaga, aby środowisko testowe było jak najbardziej zbliżone do
przewidywanego środowiska pracy oprogramowania. Konstrukcja testów traktuje
system jako całość i nie jest nastawiona na wnikanie w jego budowę.
Specyfikacja oprogramowania określająca dokładnie, co oprogramowanie ma
robić i w jakim zakresie (wydajność, niezawodność itp.), powstaje w procesie
kaskadowym w fazie analizy. W procesie iteracyjnym dokładna specyfikacja funkcji
oraz właściwości niefunkcjonalnych powstaje w fazie opracowania. Jeśli projekt jest
prowadzony z użyciem metod obiektowych, to rolę specyfikacji funkcji
oprogramowania może pełnić systemowy model przypadków użycia. Wraz ze
specyfikowaniem wymaganego zachowania systemu powinien też powstać plan jego
testowania. Przeprowadzenie testów systemowych, możliwe dopiero po zakończeniu
integracji oprogramowania, tworzy osobny etap procesu wytwórczego.
Zaprojektowanie i wykonanie testów nie wymaga znajomości szczegółów konstrukcji
oprogramowania, wymaga natomiast specjalistycznej wiedzy dotyczącej sposobów
sprawdzania właściwości niefunkcjonalnych. Z tego powodu etap testowania
systemowego realizuje zazwyczaj wyspecjalizowany zespół testujący. Wykryte błędy i
niezgodności ze specyfikacją są opisywane w raporcie problemów testowania i
przekazywane deweloperom do usunięcia.
Etap testowania systemowego składa się zwykle z szeregu odrębnych kroków, w
których sprawdzane są róże aspekty działania systemu. Szczegółowy wykaz testów
zależy od dziedziny zastosowania i specyfikacji wymagań. Typowe przykłady testów
systemu obejmują:
testy funkcjonalne (functional testing), obejmujące sprawdzenie
poprawności wykonania wszystkich funkcji systemu (np. systemowych
przypadków użycia);
testy wydajnościowe (performance testing), obejmujące sprawdzenie
działania systemu (np. czasu przetwarzania) przy nominalnym obciążeniu;
testy przeciążeniowe (stress testing), sprawdzające zachowanie systemu
w warunkach przekroczenia założonego obciążenia systemu;
testy bezpieczeństwa (security testing), których celem jest sprawdzenie
skuteczności mechanizmów ochrony systemu przed nieuprawnionym
użyciem;
testy
odporności
(recovery
testing),
sprawdzające
działanie
oprogramowania w warunkach awarii, np. nagłego wyłączenia, restartu
komputera lub odłączenia sieci;
testy zgodności (compatibility testing), sprawdzające możliwość pracy
oprogramowania na różnych platformach (np. różne systemy operacyjne
lub bazy danych itp.);
testy dokumentacji (<documentation testing), obejmujące sprawdzenie
zgodności działania oprogramowania z opisem zawartym w dokumentacji.
Testowanie akceptacyjne
Testowaniu akceptacyjnemu (acceptance testing) podlega oprogramowanie
stanowiące przedmiot dostawy dla użytkownika, zainstalowane w docelowym
środowisku pracy lub w środowisku imitującym docelowe środowisko pracy
oprogramowania. Celem testów jest sprawdzenie zgodności działania z wymaganiami
i potrzebami użytkownika. Forma testów może być podobna do testów systemowych,
jednak proces testowania nie jest już zorientowany na znajdowanie i usuwanie
defektów, lecz raczej na zademonstrowanie i zatwierdzenie produktu przez
użytkownika oraz ewentualne dostrojenie do jego rzeczywistych potrzeb.
Teoretycznie testy akceptacyjne powinien zawsze przeprowadzać użytkownik. W
praktyce jednak testy akceptacyjne przeprowadza często wykonawca pod
bezpośrednim nadzorem użytkownika lub zewnętrznej firmy działającej na jego
zlecenie. W takim przypadku użytkownik zatwierdza najpierw scenariusze testowania,
a później kontroluje wiarygodność przeprowadzenia testów. Jeśli zostaną wykryte
błędy lub niezgodności ze specyfikacją wymagań, to są one opisywane w raporcie
problemów i usuwane w określonym umową terminie.
W przypadku systemów ogólnodostępnych, które nie są przeznaczone dla
konkretnego odbiorcy, testowanie akceptacyjne może przybrać formę kontrolowanej
eksploatacji w siedzibie wytwórcy (testy alfa) oraz u wytypowanych odbiorców lub
dystrybutorów produktu (testy beta). Błędy i niezgodności wykryte podczas obu faz
testów są raportowane do wytwórcy i usuwane przed ostatecznym wypuszczeniem
produktu na rynek.
Specyfikacja wymagań użytkownika, zapisana w umowie na opracowanie
oprogramowania, jest zwykle zbyt mało dokładna, aby określić sposób testowania
akceptacyjnego. Dokładniejsza wersja specyfikacji jest wynikiem analizy wymagań
dokonanej już po zawarciu umowy lub podjęciu decyzji o opracowaniu
oprogramowania. Jeśli projekt jest prowadzony z użyciem metod obiektowych, to rolę
specyfikacji wymagań, prezentującej biznesowy punkt widzenia użytkownika, może
pełnić biznesowy model przypadków użycia. Scenariusze biznesowych przypadków
użycia są naturalnymi kandydatami na scenariusze testowania akceptacyjnego. Wraz
z opracowaniem modelu biznesowych przypadków użycia powinien też powstać plan
testowania akceptacyjnego.
Etap testowania akceptacyjnego dzieli się zwykle na kilka odrębnych kroków, w
których sprawdzane są róże aspekty jego działania, np.:
testy funkcjonalne, sprawdzające dopasowanie funkcji systemu do potrzeb
procesów biznesowych użytkownika;
testy operacyjne, sprawdzające działanie funkcji wykorzystywanych przez
administratorów systemu;
testy niefunkcjonalne, podobne lub identyczne z testami systemowymi.
2. Organizacja procesu testowania
Skuteczne przeprowadzenie testów i usunięcie błędów oprogramowania wymaga
- zwłaszcza w dużym projekcie - całego szeregu działań przygotowawczych,
związanych z planowaniem, projektowaniem i wykonaniem testów. Wynikiem tych
działań są odpowiednie dokumenty planistyczne, specyfikacje testów i raporty
testowania. Przygotowane tych dokumentów i wdrożenie ich w praktyce jest
przedmiotem procesu zarządzania testowaniem, które wchodzi w skład procesu
zarządzania projektem programistycznym.
Nie ma jednej standardowej organizacji procesu testowania i jednego
standardowego sposobu dokumentowania tego procesu. Różne metody zarządzania
projektami, a nawet różne projekty, stosują własne rozwiązania, które w dużej mierze
są pochodną wielkości projektu, konsekwencji ewentualnej awarii oprogramowania i
posiadanego budżetu. W małych, kilkuosobowych, projektach testowanie prowadzą
często deweloperzy tworzący oprogramowanie. Proces testowania jest mało
sformalizowany i ograniczony do niezbędnego minimum. Zaletą takiego podejścia jest
niski koszt, a wadą - ograniczony zakres testowania. W większych projektach
testowaniem - zwłaszcza na wyższych poziomach - zajmują się niezależne zespoły
testerów. Umożliwia to użycie lepszych metod i narzędzi, zapewnia bardziej
wszechstronne i wiarygodne przeprowadzenie testów, wymaga jednak poniesienia
dodatkowych kosztów związanych z organizacją i formalnym dokumentowaniem
całego procesu.
Treść tego podrozdziału opisuje pewien model organizacji testowania, który
wyznacza dominujący kierunek [176], ale od którego istnieją w praktyce liczne
odstępstwa.
2.1. Planowanie testów
Podstawowym dokumentem planistycznym, opisującym organizację procesu
testowania, jest plan testów (test plan). Postać tego dokumentu może być w różnych
projektach różna - w niektórych przypadkach pojedynczy dokument opisuje
testowanie na wszystkich poziomach, w innych plany testowania na różnych
poziomach są opisywane w odrębnych dokumentach. Jeżeli testy jednostkowe i
integracyjne wykonują deweloperzy, to często nie tworzy się formalnych planów, a
jedynie rezerwuje czas i zasoby w planie projektu. W takim przypadku plan testów
obejmuje tylko etapy testowania systemowego i testowania akceptacyjnego, które
pochłaniają dużo czasu i które mogą być wykonane dopiero po zakończeniu
implementacji i integracji oprogramowania. Ze względu na konieczność uzgodnienia
przebiegu testowania akceptacyjnego z użytkownikiem plan testowania
akceptacyjnego jest często odrębnym dokumentem, którego wstępna wersja
obejmująca przede wszystkim harmonogram testowania jest często zapisana w
umowie.
Treść planu testów opisuje zakres testów, strategię testowania i raportowania
błędów, niezbędne zasoby i harmonogram działań.
Definicja zakresu obejmuje identyfikację testowanych produktów, określenie
wymagań (właściwości), które będą testowane, oraz jawne wskazanie wymagań,
które testowane nie będą.
Strategia testowania określa środki, które zostaną użyte do zapewnienia należytej
jakości testowania różnych grup wymagań. Mieści się w tym wskazanie działań i
metod testowania (np. metody pomiaru wydajności), zdefiniowanie kryteriów
zakończenia testów (np. z wykorzystaniem metryk opisanych w podrozdziale 3) oraz
określenie kryteriów oceny pozytywnego lub negatywnego wyniku testów
oprogramowania lub jego komponentów. Proste kryteria oceny wyniku testów
polegają na klasyfikacji błędów na kategorie odzwierciedlające stopień ich
szkodliwości - np.: awarie, błędy poważne, błędy drobne - a następnie określeniu
dopuszczalnej liczby błędów każdego rodzaju ujawnionych podczas testów. Bardziej
zaawansowane metody korzystają z obliczeń probabilistycznych umożliwiających
określenie niezawodności oprogramowania, wyrażonej w postaci oczekiwanej
wartości czasu między wystąpieniami dwóch kolejnych błędów. Opis strategii
testowania powinien być na tyle dokładny, aby umożliwić identyfikację głównych
zadań oraz ocenę zasobów i czasu potrzebnego do ich wykonania.
Zasoby niezbędne do przeprowadzenia testów obejmują środowisko testowe,
zespół lub zespoły wykonawców o różnych kwalifikacjach oraz wymaganą przestrzeń
biurową. Środowisko testowe obejmuje system komputerowy dedykowany do
prowadzenia testów wraz z oprogramowaniem wspomagającym. Planując proces
testowania, trzeba określić konieczne do wykonania zakupy, zakres niezbędnych do
wykonania prac instalacyjnych i konfiguracyjnych oraz potrzebę wytworzenia
sterowników i namiastek testowych. Jeśli testowanie ma być przeprowadzone na
danych pochodzących z systemu produkcyjnego, to plan testów powinien określić
metodę pozyskania i przeniesienia danych. Wielkość i sposób organizacji zespołu
testującego zależą od wielkości projektu. W małych projektach wystarczy wskazać
osoby przydzielone do testowania. W dużych projektach istnieje potrzeba
zdefiniowania struktury organizacyjnej, podporządkowanej kierownikowi projektu i
obejmującej projektantów i wykonawców testów (rys. 2). Poza wskazaniem potrzeb
plan testowania powinien wyraźnie określić odpowiedzialność za dostarczenie
testowanych programów, przygotowanie środowiska, zarządzanie, projektowanie i
wykonanie testów oraz usunięcie błędów.
Rysunek 2. Organizacja zespołu testującego
Warunkiem rozpoczęcia testowania jest wytworzenie testowanego produktu.
Czas przeznaczony na testowanie zależy od wielkości oprogramowania, stopnia
automatyzacji testów oraz liczby jednocześnie pracujących testerów. Dlatego
harmonogram testowania jest ściśle związany z harmonogramem opracowania
oprogramowania oraz z wielkością przydzielonych do projektu zasobów.
Najtrudniejszym zadaniem podczas tworzenia harmonogramu jest oszacowanie
pracochłonności testowania. Znając oszacowanie pracochłonności, można albo
narzucić z góry czas testowania i dobrać odpowiednią wielkość zespołu, albo przyjąć
pewną wielkość zespołu i obliczyć niezbędny czas trwania testów.
2.2. Przygotowanie testów
Skuteczne przeprowadzenie testów oprogramowania wymaga zaprojektowania
takiego zestawu testów, który zapewni kompletne i wiarygodne sprawdzenie
produktu, zgodnie ze strategią określoną w planie testowania. Najważniejszym
dokumentem projektowym opisującym planowane testy oprogramowania jest
specyfikacja testów (test specification). Treścią tego dokumentu jest lista przypadków
testowych wraz ze wskazaniem wymagań (np. obecności pewnych danych w bazie
danych) lub ograniczeń ich wykonania. Jeśli wykonanie przypadku testowego
wymaga określonej sekwencji działań testera, np. wypełnienia pól ekranu,
zaznaczenia pól wyboru i kliknięcia przycisku akceptacji, to sekwencja tych czynności,
opisana w specyfikacji testów, tworzy scenariusz przypadku testowego (test case
scenario).
Struktura i postać specyfikacji testów zależą od wielkości projektu. W małych
projektach specyfikacja testów może liczyć kilka kartek zawierających opis wszystkich
przypadków testowych. W takich przypadkach specyfikacja testów jest niekiedy
włączana w skład planu testów. W projektach dużych specyfikacja testów może mieć
postać kilku odrębnych dokumentów poświęconych testowaniu różnych obszarów
funkcjonalnych oraz różnych właściwości niefunkcjonalnych. Co więcej, w dużych,
sformalizowanych projektach bezpośrednie przejście od planu do specyfikacji testów
jest niejednokrotnie zbyt trudne do wykonania. W takich przypadkach opracowuje się
często dodatkowy dokument projektu testów (test design), który wiąże strategię
testowania, opracowaną w planie testów, z dokładnym opisem przypadków
testowych, zapisanym w specyfikacji testów. Powiązanie dokumentacji testów z
dokumentacją projektu oprogramowania jest pokazane na rys. 3.
Rysunek 3. Relacja dokumentów projektowych i testowych [176]
Specyfikacja testów jest dokumentem projektowym procesu testowania,
opisującym przypadki testowe w takiej kolejności i w taki sposób, aby umożliwić łatwe
sprawdzenie kompletności testów. Na przykład, w specyfikacji testów systemu
wspomagającego działanie urzędu administracji publicznej można opisywać kolejno:
testy wszystkich metod wprowadzania dokumentów, testy wszystkich sposobów
otwierania sprawy petenta, testy wszystkich form kontroli itd. Nie jest to kolejność
wygodna do przeprowadzenia testów. Ponieważ wykonanie wcześniejszej fazy
obsługi tej samej sprawy (np. wprowadzenie dokumentu) warunkuje możliwość
wykonania fazy późniejszej (np. kontroli tego dokumentu), więc wygodniej jest
prowadzić testy w kolejności zgodnej z logiką procesu biznesowego, wspomaganego
przez oprogramowanie. Tę kolejność oraz sposób przeprowadzenia testów opisuje
odrębny dokument nazywany procedurą testowania (test procedure).
Procedura testowania określa początkowy stan systemu w chwili rozpoczęcia
testowania oraz listę zestawów testów do wykonania (rys. 4). Definicja zestawu
testów zawiera listę scenariuszy przypadków testowych, wykonywanych w ustalonej
kolejności, oraz listę zbiorów danych testowych używanych podczas wykonania tych
scenariuszy. Każdy scenariusz przypadku testowego składa się z sekwencji kroków, z
których każdy obejmuje jedną, dobrze określoną czynność. W przykładowej
procedurze testowania jest nią wywołanie wskazanej funkcji menu, wybranie
wskazanej opcji lub wypełnienie pól ekranu wartościami pobranymi z przygotowanego
zbioru danych testowych.
Zbiory danych testowych mogą mieć postać tabel wypełnionych wartościami
danych wprowadzanych podczas wykonania scenariusza testowego, dołączonych do
dokumentu procedury testowania. Częściej jednak są one przekazywane testerom w
postaci zbioru danych zapisanego na nośniku maszynowym. Procedura testowania
uzupełniona zbiorami danych testowych oraz opisem oczekiwanych wyników tworzy
dokładną instrukcję wykonania testów, która nie wymaga żadnych dodatkowych
dokumentów wyjaśniających.
Procedura testowania funkcjonalności płatności obszarowych
1.
Początkowy stan systemu:
.... opis konfiguracji systemu, sieci i bazy danych
2.
Przebieg wykonania (zestawy testów):
Z-1 Wprowadzenie i kontrola danych wniosku
Z-2 Weryfikacja treści wniosku
Z-3 Naliczenie płatności obszarowej
3.
Definicje zestawów testów
Z-1 Wprowadzenie i kontrola danych wniosku (przypadki testowe):
P-1 Przyjęcie dokumentu wniosku (zbiory danych testowych D-1, D-2)
P-2 Wprowadzenie danych wniosku (zbiory danych testowych D-1, D-2)
P-3 Zatwierdzenie danych wniosku
P-4 Przeprowadzenie kontroli danych
Z-2 Weryfikacja treści wniosku
Z-3 Naliczenie płatności obszarowej
4.
Scenariusze przypadków testowych
P-1 Przyjęcie dokumentu wniosku
1. Wybranie funkcji Przyjęcie dokumentu
2. Zaznaczenie pola Wniosek obszarowy
3. Wybranie funkcji Utworzenie sprawy
4. Wprowadzenie opisu dokumentu (data, nadawca)
Wybranie opcji Zatwierdzenie sprawy
P-2 Wprowadzenie danych wniosku
Rysunek 4. Procedura testowania jednej z funkcjonalności systemu wspierającego dopłaty rolne
Warto zwrócić uwagę, że poszczególne przypadki testowe nie są wymienione w
procedurze testowania w sposób jawny (jest ich zbyt dużo). Zamiast tego procedura
definiuje scenariusze przypadków użycia oraz zbiory danych testowych. Pojedynczy
przypadek testowy odpowiada jednemu wykonaniu scenariusza przypadku testowego
z użyciem konkretnych danych ze zbioru danych testowych przypisanych do tego
scenariusza.
2.3. Testowanie a usuwanie defektów
Błędem jest każdy nieprawidłowy, czyli niezgodny ze specyfikacją, wynik
działania programu. Przyczyną błędów są tkwiące w programie defekty. Celem etapu
testowania jest wykrycie błędów działania programu, a następnie lokalizacja i
usunięcie powodujących te błędy defektów. Czynności wykrywania błędów
(testowanie) oraz usuwania defektów (poprawianie programu) są różne i mogą być
różnie usytuowane w czasie.
Najprostszy sposób działania polega na znajdowaniu i usuwaniu defektu
natychmiast po wykryciu błędu - jest to postępowanie naturalne dla dewelopera
samodzielnie testującego swój program. Nie jest to jednak strategia dobra dla
zespołu, w którym prace powinny przebiegać równolegle. Można równolegle testować
program na kilku stanowiskach i wykrywać w tym czasie różne błędy, ale nie można
równolegle tych błędów poprawiać - takie działanie mogłoby doprowadzić do
powstania kilku różnych wersji programu, z których każda zawierałaby różne
poprawki. Dlatego zwykle przyjmuje się inny sposób działania, zgodnie z którym
najpierw testuje się program w serii testów, rejestrując przy tym błędy, a potem
analizuje się kod programu, odnajdując i usuwając defekty.
Wszystkie niezgodności wyników z oczekiwaniami, zaobserwowane podczas
wykonywania testów, są zapisywane w raporcie problemów (test incident report).
Nazwa tego dokumentu podkreśla fakt, że nie każda niezgodność wyniku z
oczekiwaniem musi być błędem programu. Błędy mogą też tkwić w testach, w
dokumentacji programu i w dokumentacji testów. Mylić mogą się także testerzy.
Dlatego każdy wykryty i opisany w raporcie problem musi być poddany analizie, której
wynikiem jest kwalifikacja problemu. Tylko problemy zakwalifikowane jako błędy
programu są przekazywane deweloperom do usunięcia (rys. 5).
Rysunek 5. Testowanie, analiza problemów i usuwanie błędów
Po wykonaniu serii testów i wprowadzeniu poprawek powstaje kolejna wersja
programu, która jest poddawana podwójnemu sprawdzeniu:
czy wszystkie dotychczas działające funkcje programu działają nadal (testy
regresji),
czy wszystkie wykryte błędy zostały rzeczywiście usunięte (retesty).
Zestaw testów regresji (regression test) obejmuje te testy, które były wykonywane
bezbłędnie przed rozpoczęciem wprowadzania poprawek. Zestaw retestów obejmuje
te testy, których wyniki zostały zakwalifikowane jako błędy. Błędne wyniki
którejkolwiek z tych dwóch grup testów powodują ponowne odesłanie produktu do
poprawek. Poprawne wyniki wszystkich testów kończą bieżący cykl testowania i
otwierają drogę do kolejnych serii testów (rys. 6). Etap testowania kończy się po
spełnieniu kryteriów zakończenia sformułowanych w planie testów.
Cykl testowania
Kolejny cykl
Seria testów
Poprawki
Testy regresji
Retesty
Koleina seria
testów
►
Rysunek 6. Testowanie i usuwanie defektów programu
3. Metryki
Testowanie oprogramowania jest długotrwałym procesem, od którego w dużym
stopniu zależy jakość finalnego produktu i który pochłania znaczną część zasobów
projektu. Aby móc efektywnie planować i zarządzać wykonaniem tego procesu,
potrzebny jest jakiś sposób pomiaru jakości testów oraz sposób pomiaru postępu prac
w czasie trwania procesu testowania. Narzędziem pomiaru są metryki testowania,
czyli ilościowe miary odzwierciedlające różne cechy zaprojektowanych, lub już
wykonanych, testów. Stworzenie możliwości oceny jakości testów otwiera drogę do
planowania jakości testowania i zapobiegania przypadkom niewiarygodnej lub
niedostatecznej weryfikacji programu.
Jedną z najczęściej stosowanych metod pomiaru i poprawy jakości testów jest
analiza pokrycia kodu (code coverage analysis). Celem tej analizy jest ilościowa
ocena kompletności testów, znalezienie obszarów kodu, które nie są sprawdzane
przez testy, oraz wykrycie nadmiarowych przypadków testowych, które nie poprawiają
jakości testowania, a pochłaniają czas i zasoby. Konsekwencją negatywnej oceny
może być zaprojektowanie dodatkowych przypadków testowych obejmujących
sprawdzenie pominiętych obszarów programu.
Koncepcyjnym narzędziem pomiaru są metryki pokrycia kodu (code coverage
metrics), służące do określenia stosunku obszaru tej części programu, która została
poddana testom, do całości programu. Liczbowa wartość metryki może być
wykorzystana jako miara jakości zestawu testów - tym lepsza, im bliższa wartości
pełnego pokrycia kodu (100%). Przyrost wartości tej metryki w okresie wykonywania
testów może stać się miarą postępu procesu testowania. Metryki pokrycia kodu
znajdują zastosowanie przede wszystkim na poziomie testowania jednostkowego.
Podobną w konstrukcji metodę można wykorzystać do analizy stopnia pokrycia
wymagań testami funkcjonalnymi. Celem analizy jest ilościowa ocena stopnia
kompletności testowania wymagań funkcjonalnych oraz znalezienie obszarów
specyfikacji wymagań, które nie są sprawdzane przez testy. Narzędziem pomiaru
kompletności testów są odpowiednie metryki, oceniające stosunek liczby zestawów i
przypadków testowych do liczby wymagań zdefiniowanych w sposób określony przez
stosowaną metodę analizy. Metryki pokrycia wymagań znajdują zastosowanie przede
wszystkim na poziomie testowania systemowego lub testowania akceptacyjnego.
Wykorzystanie metryk pokrycia nie jest jedynym sposobem oceny jakości procesu
testowania. Prócz nich stosuje się także metryki oparte na bezpośrednim pomiarze
liczby wykonanych przypadków testowych, liczby wykrytych błędów lub liczby błędów
usuniętych. Jeszcze inna, szybko rozwijająca się grupa metod wykorzystuje
eksperymentalne sposoby oceny efektywności wykrywania błędów przez testy oraz
szacowania liczby niewykrytych błędów, jakie jeszcze pozostały w programie.
3.1. Metryki pokrycia kodu
Analiza pokrycia kodu jest elementem metody testowania „białej skrzynki",
zgodnie z którą do projektowania i oceniania jakości testów wykorzystuje się
znajomość wewnętrznej struktury programu. Słabością tego podejścia jest brak
jednolitej definicji pokrycia, prowadzący do powstania różnych metryk pokrycia kodu,
oceniających jakość testów w stosunku do różnych aspektów programu. Istotną
zaletą wszystkich metryk pokrycia kodu jest łatwość interpretacji wartości tych metryk,
która może zmieniać się w zakresie od 0 do 100%.
Pokrycie bloków instrukcji
Blokiem jest ciąg instrukcji programu, niezawierający instrukcji powodujących
rozgałęzienie alternatywnych dróg wykonania - tzn. instrukcji
if switch, for itp.
Wartością metryki pokrycia bloków instrukcji (basic block coverage) jest stosunek
liczby bloków przetestowanych do liczby wszystkich bloków programu. Na przykład,
następujący fragment programu w języku C zawiera dwa bloki instrukcji,
odpowiadające dwóm gałęziom instrukcji if
if ( a!=0 )
x = -b/a;
printf("x=%f\n",x);
else
printf("Niepoprawne dane\n");
Pełne pokrycie bloków instrukcji tego fragmentu programu wymaga użycia co
najmniej dwóch przypadków testowych, z których każdy spowoduje wykonanie jedne
gałęzi instrukcji if. Jeden przypadek testowy zapewni tylko 50% pokrycia.
Odmianą metryki pokrycia bloków jest metryka pokrycia instrukcji kodu, które
wartością jest stosunek liczby przetestowanych instrukcji do liczby wszystkich
instrukcji testowanej jednostki programu.
Podstawowymi zaletami obydwu metryk są intuicyjna zrozumiałość, prostota
pomiaru oraz możliwość zastosowania wprost do wynikowego kodu programu, a nic
tylko do kodu źródłowego. Zasadniczą wadą jest słabe odzwierciedlenie jakości
sprawdzania warunków. Na przykład, każdy przypadek testowy z wartością b = C
zapewni pełne 100% pokrycia instrukcji programu:
float b,a = 0;
if ( b==0 ) a = 1;
x = b/a;
ale tylko test z wartością b != 0 pozwoli wykryć błąd prowadzący do zatrzymania
programu z powodu próby dzielenia przez zero. Metryka pokrycia bloków nie
odzwierciedla też w żaden sposób jakości sprawdzania różnych wariantów obliczania
złożonych warunków instrukcji if ani warunków zakończenia pętli programu.
Pokrycie decyzji
Decyzją w metryce pokrycia decyzji (decision coverage) jest każda możliwa
wartość warunku w instrukcji powodującej rozgałęzienie programu, również w
instrukcji switch. Wartością metryki jest stosunek liczby przetestowanych wyjść z
instrukcji rozgałęziających do liczby wszystkich wyjść z tych instrukcji. Na przykład,
ostatni program zawiera jedną instrukcję if której warunek może przyjąć jedną z
dwóch wartości true lub false, kierując wykonanie na jedną z dwóch różnych dróg
programu (rys. 7a). Pełne pokrycie decyzji tego fragmentu programu wymaga więc
użycia co najmniej dwóch przypadków testowych, z których każdy spowoduje
obliczenie innej wartości warunku. Program pokazany w postaci sieci działań na rys.
7b zawiera dwie instrukcje warunkowe. Przetestowanie wszystkich decyzji tego
programu wymaga wykonania trzech przypadków testowych, z wartościami danych
wejściowych: delta>0, delta=0 i delta<0, które spowodują przejście wykonania przez
każde wyjście obydwu instrukcji warunkowych.
Metryka pokrycia decyzji wymaga zwykle większej liczby przypadków testowych
do pełnego przetestowania programu niż metryka pokrycia bloków instrukcji. W
zamian za to zapewnia nie gorsze sprawdzenie bloków i lepsze sprawdzenie sposobu
testowania warunków. Wadą tej metryki jest brak wrażliwości na sposób testowania
warunków złożonych, które nie w każdym przebiegu programu są obliczane do końca.
Na przykład, następujący fragment programu w języku C zawiera warunek złożony,
którego ostatni człon będzie obliczony tylko wtedy, gdy dwa pierwsze nie określą
wartości warunku:
if ( a>0 || ( b>0 && fun(b)>0 ) ) printf("Ok\n");
Tak więc dwa przypadki testowe z wartościami danych wejściowych np. takich:
a=5, b=3
a=0, b=-2
spowodują obliczenie wartości warunku równej true (pierwszy test) i fase (drugi
test), co zapewni 100% pokrycia decyzji bez wywołania funkcji
fun.
Rysunek 7. Sieci działań ilustrujące drogi przejścia i decyzje programu
Pokrycie ścieżek
Ścieżką jest każda sekwencja instrukcji prowadząca od początku do końca
programu. Wartością metryki pokrycia ścieżek (path coverage) jest stosunek liczby
ścieżek sprawdzonych w testach do liczby wszystkich ścieżek istniejących w
programie.
Zgodnie z tą definicją niekompletne obliczanie warunków złożonych przez
kompilatory niektórych języków - np. C, C++ i Javy - tworzy nowe ścieżki wykonania,
które nie są jawnie pokazane w tekście programu. Liczba wszystkich ścieżek
wykonania warunków złożonych odpowiada liczbie wszystkich wykonalnych
kombinacji wartości warunków, z których jest zbudowany warunek złożony. W ten
sposób fragment programu podany powyżej zawiera cztery możliwe ścieżki.
1. Obliczenie a>0, po czym wydruk Ok.
2. Obliczenie a<0 i b>0, po czym wydruk Ok.
3. Obliczenie a<0 i b<0 i f un (b )>0, po czym wydruk Ok.
4. Obliczenie a<0 i b<0 i fun (b )<0, brak wydruku.
Dwa poprzednie przypadki testowe sprawdzają tylko dwie ścieżki, co zapewnia
50% pokrycia ścieżek programu. Pełne pokrycie ścieżek tego programu wymaga
wykonania aż czterech testów. Metryka pokrycia ścieżek wymaga zwykle większej
liczby testów do uzyskania pełnego pokrycia niż inne metryki, co zapewnia lepszą
jakość testowania.
Z drugiej strony trudnym problemem dla praktycznego wykorzystania tej metryki
są pętle, których obecność wprowadza bardzo dużą, a czasem nieskończoną, liczbę
możliwych ścieżek przejścia programu. Wykonanie programu z dwoma obiegami pętli
przebiega po innej ścieżce niż wykonanie z trzema, czterema itd. obiegami pętli. Z
tego powodu metryka pokrycia ścieżek bywa stosowana w praktyce w
zmodyfikowanej wersji, w której rozważa się tylko ograniczoną liczbę obiegów pętli.
Przytoczone tu metryki pokrycia nie wyczerpują wszystkich możliwości. Istnieje
szereg innych metryk, których konstrukcja bierze pod uwagę np. stopień pokrycia
wywołań funkcji, dróg od przypisania wartości zmiennej do odczytania tej wartości,
odwołań do komórek tablic i wiele innych. Głównym obszarem zastosowania tych
metryk jest planowanie i ocena testów jednostkowych oprogramowania wpływającego
na bezpieczeństwo ludzi. Na przykład, pewien wariant metryki pokrycia dróg
programu, znany pod nazwą kombinowanej metryki pokrycia warunków i decyzji
(modified condition/decision coverage), jest wymagany do oceny testowania
oprogramowania używanego w lotnictwie [172].
3.2. Metryki pokrycia wymagań
Analiza pokrycia wymagań jest elementem metody testowania „czarnej skrzynki",
w której nie korzysta się z wiedzy dotyczącej wewnętrznej struktury programu.
Zarówno projekt testów, jak i ocena jakości testowania odwołują się w tym podejściu
do specyfikacji wymagań traktowanej jako wzorzec poprawnego zachowania
programu. Miarą jakości testów jest stopień pokrycia wymagań przypadkami
testowymi. Zaletą takiego podejścia jest bezpośrednie skojarzenie oceny jakości
testów z oceną stopnia spełnienia wymagań, prowadzące do wysokiej
akceptowalności wyników oceny przez użytkowników. Wadą metody jest brak
jednolitej definicji pokrycia wymagań oraz brak standaryzacji zarówno samych metryk,
jak i ich nazewnictwa.
Pokrycie wymagań
Wartością metryki pokrycia wymagań (requirements coverage) jest stosunek
liczby przetestowanych wymagań do liczby wszystkich wymagań zapisanych w
specyfikacji. Liczba przypadków testowych niezbędnych do przetestowania
pojedynczego wymagania zależy od jego charakteru i stopnia złożoności.
Sposób rozumienia pojęcia „wymagania" w tej definicji zależy od metody przyjętej
do określania wymagań. Jeżeli wymagania są określone tekstowo, w postaci
wypunktowanej listy wymagań, to za wymaganie można uznać każdy punkt listy.
Słabością tego określenia jest niska precyzja i potencjalny brak rozłączności różnych
punktów listy. Zaletą jest możliwość uwzględnienia w wartości metryki zarówno
wymagań funkcjonalnych, jak i niefunkcjonalnych. Jeśli modelem wymagań jest
hierarchia funkcji, to za wymaganie można uznać każdą funkcję położoną na
najniższym poziomie hierarchii. Jeśli wymagania są modelowane w postaci
przypadków użycia, to wymaganiem jest każdy zdefiniowany scenariusz przypadku
użycia.
Pokrycie błędów
Metryki pokrycia wymagań koncentrują się na kontroli poprawnego zachowania
programu. Dla stabilności aplikacji ważne jest również zachowanie w przypadku błędu
- pomyłki operatora, awarii urządzenia peryferyjnego, błędnych danych wejściowych
lub wewnętrznego błędu programu. Na ogół oprogramowanie wykrywa i sygnalizuje
przynajmniej część takich sytuacji za pomocą odpowiedniego komunikatu błędu. Lista
wykrywanych błędów powinna być jawnie podana w dokumentacji programu.
Wartością metryki pokrycia błędów (error coverage) jest stosunek liczby błędów,
których wykrycie zademonstrowano podczas testów, do liczby wszystkich błędów
wykrywanych zgodnie z dokumentacją programu.
Testowanie przypadków użycia
Specyfikacja oprogramowania w postaci modelu przypadków użycia silnie
wspiera proces projektowania testów akceptacyjnych. Bardzo często istnieje
bezpośrednia odpowiedniość między biznesowymi przypadkami użycia a zestawami
testów wchodzących w skład procedury testowej oraz między systemowymi
przypadkami użycia a przypadkami testowymi. W takiej sytuacji celowe może być
obliczenie następujących metryk:
pokrycia biznesowych przypadków użycia zestawami testów,
pokrycia systemowych przypadków użycia scenariuszami przypadków
testowych,
pokrycia scenariuszy systemowych przypadków użycia przez przypadki
testowe.
Przytoczone metryki można obliczyć albo zbiorczo, w postaci wartości średnich -
np. stosunek liczby wszystkich scenariuszy przypadków testowych wymienionych w
procedurze testowej do liczby wszystkich przypadków użycia, albo odrębnie dla
każdego przypadku użycia. Niezależnie od przyjętej metody liczenia zbyt niskie
wartości metryk świadczą o niskiej jakości testowania. Wartością graniczną dla oceny
liczby przypadków testowych, niezbędnych do przetestowania przypadku użycia, jest
liczba jego scenariuszy. Jeżeli liczba przypadków testowych jest mniejsza od liczby
scenariuszy przypadku użycia, to znaczy, że pewne scenariusze zostały w procesie
testowania pominięte.
3.3. Inne metryki
Testowanie jest końcową czynnością całego procesu wytwórczego, lub jego
kolejnej iteracji, wykonywaną po implementacji programu. Dlatego przebieg procesu
testowania - w tym czas trwania oraz liczba i rodzaj wykrytych błędów - zależy w
dużym stopniu nie tylko od jakości testów, ale także od jakości wytworzonego kodu.
Ocena obydwu tych czynników jest ważnym elementem wspomagającym działanie
kierownika projektu. Z tego powodu podczas trwania testów oblicza się często szereg
różnych metryk umożliwiających pomiar i ocenę zarówno procesu testowania i
usuwania błędów, jak i jakości testowanego kodu.
Liczba wykrytych defektów
Wartość tej metryki, rejestrowana regularnie podczas trwania testów i
obserwowana systematycznie podczas różnych projektów, pozwala ocenić:
początkową jakość kodu wytworzonego przez programistów i
deweloperów,
przyrost tej jakości następujący w wyniku usuwania błędów,
efektywność różnych metod i etapów testowania.
Typowy przebieg zmiany łącznej liczby defektów kodu wykrytych podczas trwania
testów, pokazany na rys. 8, dzieli się na trzy wyraźnie różne okresy. Okres
rozruchowy, o niewielkiej liczbie wykrytych defektów, okres środkowy, w którym liczba
wykrytych defektów szybko przyrasta, oraz okres stabilizacji oprogramowania, w
którym na skutek usunięcia większości defektów liczba wykrytych defektów niemal
przestaje wzrastać. Osiągnięcie stadium stabilizacji liczby wykrytych defektów jest
jednym z ważnych kryteriów zakończenia testów.
Rysunek 8. Łączna liczba defektów wykrytych podczas testowania Liczba defektów komponentu
Wartości tej metryki, równe łącznej liczbie defektów wykrytych podczas
testowania poszczególnych komponentów oprogramowania, mają dwojakie
znaczenie. Po pierwsze, duża liczba defektów komponentu, testowanego podobnie
jak wszystkie inne komponenty, może świadczyć o jego wysokiej złożoności lub o
niskiej jakości jego kodu. Porównanie różnych komponentów może więc pomóc w
ustaleniu komponentów ryzykownych, wymagających specjalnej uwagi w procesie
integracji oraz podczas późniejszej konserwacji oprogramowania. Po drugie, duża
liczba defektów kodu może świadczyć o niskiej jakości pracy lub przeciążeniu
opracowujących ten komponent deweloperów. To z kolei może pomóc kierownikowi
projektu w podjęciu decyzji o przemieszczeniu zasobów.
Pewnym wariantem tej i poprzedniej metryki jest zliczanie defektów w rozbiciu na
poszczególne testy lub zestawy testów. Porównanie wyników pozwala wybrać testy
wykrywające najwięcej defektów. Może to pomóc w konstruowaniu zbioru testów
regresji lub zbioru testów zaufania (smoke tests), używanych do szybkiego
sprawdzenia poprawności programu podczas konserwacji.
Częstość defektów
Metryka częstości defektów należy do najpopularniejszych i najczęściej
stosowanych miar oceny jakości kodu programu przeznaczonego do zastosowań
komercyjnych. Konstrukcja tej metryki opiera się na intuicyjnym założeniu, że liczba
defektów programu rośnie wraz ze wzrostem rozmiaru jego kodu. Aby zapewnić
porównywalność wyniku niezależnie od wielkości programu, przyjmuje się, że
wartością metryki jest stosunek liczby defektów do liczby wierszy kodu programu. Tak
zdefiniowaną metrykę można obliczać zarówno dla całości oprogramowania, jak i dla
poszczególnych komponentów. Jednym z przykładów zastosowania jest pomiar
przyrostu jakości kolejnych, poprawionych wersji programu w stosunku do wersji
początkowej.
Problemem podczas obliczania tej metryki jest brak ogólnie przyjętej definicji
„wiersza kodu programu". Można tę metrykę obliczać w stosunku do liczby instrukcji
kodu wynikowego (np. w asemblerze) albo do liczby wierszy kodu źródłowego (np. w
C, C# lub w Javie). Przyjmując za podstawę program źródłowy, można liczyć
wszystkie wiersze programu, wraz komentarzami i nagłówkami, albo tylko instrukcje
wykonalne i deklaracje danych. Jeżeli ta sama instrukcja lub deklaracja jest
rozmieszczona w kilku wierszach tekstu, to można ją liczyć jako jedną instrukcję lub
deklarację, albo jako kilka wierszy tekstu. Wszystkie te wątpliwości nie obniżają
jednak wartości metryki jako narzędzia wytwórcy oprogramowania. Każda firma
informatyczna może określić własny sposób liczenia metryki częstości defektów i
używać jej do porównywania jakości wytworzonego przez siebie kodu.
Procent defektów poprawionych
Wartość tej metryki, równa stosunkowi liczby defektów poprawionych do liczby
defektów wykrytych podczas testów, pozwala ocenić postęp prac nad usuwaniem
defektów programu. Wynikiem tej oceny może być decyzja o przejściu do kolejnego
etapu testów albo próbnej eksploatacji oprogramowania.
Szacowanie liczby wszystkich defektów
Przydatność metryk opartych na zliczaniu wykrytych defektów programu jest
ograniczona z powodu braku znajomości liczby defektów znajdujących się
początkowo w programie. Czy z faktu wykrycia i usunięcia np. 500 defektów wynika,
że program jest już godny zaufania? Trudno powiedzieć - jeśli program zawierał
początkowo 500 defektów, to na pewno tak, ale jeśli zawierał ich 1000, to raczej nie.
Metoda zasiewu defektów (fault seeding) jest próbą eksperymentalnego
oszacowania liczby wszystkich defektów programu. Eksperyment polega na
wprowadzeniu do programu pewnej znanej liczby defektów - nazywanych dalej
defektami sztucznymi - i odnajdywaniu ich podczas testowania na równi z defektami
prawdziwymi. Jeśli przyjąć, że oba rodzaje defektów są znajdowane tak samo
skutecznie, to stosunek liczby wykrytych defektów sztucznych (n
s
) do liczby
wszystkich defektów sztucznych (N
s
) powinien być taki sam jak stosunek liczby
wykrytych defektów prawdziwych (n
p
) do liczby wszystkich defektów prawdziwych
(N
p
):
n
s
/ N
s
= n
p
/ N
p
Trzy z tych wartości: N
s
, n
s
, n
p
są po pewnym czasie testowania znane. Na ich
podstawie można obliczyć czwartą:
N
p
= N
s
* n
p
/ n
s
Wartość N
p
można traktować jako oszacowanie rzeczywistej liczby wszystkich
defektów programu. Wiarygodność metody jest tym większa, im bardziej zbliżone do
siebie są typy defektów prawdziwych i wprowadzonych do programu defektów
sztucznych.
4. Metody testowania
Celem testowania jest doświadczalne porównanie wyników działania programu ze
wzorcem, którym jest specyfikacja tego programu. Jeśli wyniki są zgodne, to program
uznaje się za poprawny. Problem polega na tym, że ze względu na olbrzymią liczbę
różnych sposobów działania programu, wyznaczoną przez liczbę wszystkich
możliwych kombinacji wartości danych wejściowych, nie da się sprawdzić działania
programu we wszystkich sytuacjach w rozsądnym czasie. Tym, co jest możliwe, jest
wybranie do testów pewnego reprezentatywnego zbioru wartości danych i
sprawdzenie działania programu dla tej ograniczonej liczby przypadków testowych.
Głównym zadaniem etapu projektowania testów jest wybór takiego zbioru wartości
danych, który umożliwi dostatecznie wiarygodne sprawdzenie programu w ramach
posiadanego czasu i zasobów.
Metodę projektowania testów można oprzeć na specyfikacji, która nie określa
wewnętrznej budowy programu. Różne przypadki testowe mogą tu reprezentować
różne sposoby działania opisane w specyfikacji programu (jak różne scenariusze,
różne reguły biznesowe itp.). Albo przeciwnie, znając budowę programu, można
wykorzystać tę wiedzę, aby w ograniczonej liczbie kroków przetestować wszystkie
jego elementy składowe i ich połączenia. Różne przypadki testowe mogą tu
reprezentować różne możliwe drogi przetwarzania danych wejściowych w wyniki.
Zależnie od wybranego podejścia mówi się wtedy o testowaniu czarnej skrzynki, o
nieznanym wnętrzu, lub białej skrzynki, o znanej zawartości. Obydwa podejścia mają
swoje zalety i wady i obydwa są stosowane.
Metoda białej skrzynki (white box testing) umożliwia dokładne przetestowanie
wszystkich składników oprogramowania oraz ułatwia znalezienie i usunięcie błędów.
Oparcie budowy testów na budowie programu oznacza jednak oparcie się na czymś,
co być może jest błędne. To z kolei może uniemożliwić odnalezienie wszystkich
defektów - np. jeżeli pewnych funkcji w programie nie ma, to bazując tylko na
strukturze programu, nie będzie można tego braku odnaleźć.
Metoda czarnej skrzynki (black box testing) jest natomiast odpowiednia do
testowania przez użytkownika, który ani nie zna, ani nie chce znać wewnętrznej
struktury programu. Celem użytkownika jest upewnienie się, czy program wykonuje
potrzebne mu funkcje i czy robi to wystarczająco dobrze. Znaczenie może mieć także
sprawdzenie, jak zachowuje się program obsługiwany niezgodnie z intencjami
użytkownika.
Niezależnie od metody testowania, wykorzystanej na danym poziomie testów,
konieczne jest zawsze przygotowanie dostatecznie dużej liczby danych testowych,
które posłużą do sprawdzenia poprawności działania programu. Analiza specyfikacji
lub struktury programu, niezbędna do wytypowania właściwych wartości danych, jest
działaniem drogim, zwłaszcza na poziomie testów jednostkowych, w których
testowaniu mogą podlegać tysiące, a nawet dziesiątki tysięcy odrębnych funkcji lub
klas programu. Dlatego często generuje się dane w sposób losowy i ocenia
efektywność testowania za pomocą metryk i metod opisanych w podrozdziale 3.
Dodatkową zaletą losowej generacji danych testowych jest statystyczna niezależność
od siebie (brak korelacji) poszczególnych przypadków testowych.
4.1 Testowanie czarnej skrzynki
Testowanie metodą czarnej skrzynki jest próbą sprawdzenia poprawności
zachowania programu we wszystkich sytuacjach, jakie można wyróżnić na podstawie
jego specyfikacji. Projektowanie przypadków testowych polega na wybraniu zbioru
poprawnych i niepoprawnych wartości danych wejściowych zapewniających pokrycie
całej przestrzeni zachowań programu. Ocenę kompletności testowania można
wyrazić za pomocą metryk pokrycia wymagań, opisanych w punkcie 3.2. Metoda
umożliwia wykrycie odstępstw i braków implementacji, ale nie gwarantuje
wyczerpującego sprawdzenia wszystkich wewnętrznych elementów programu.
Metoda jest stosowana przede wszystkim na wyższych poziomach testowania, na
których występują programy o wielkiej złożoności wewnętrznej.
Deterministyczne projektowanie przypadków testowych polega na poszukiwani
jak najmniejszego zbioru wartości danych wejściowych, takiego który pozwoli
zademonstrować wszystkie rodzaje różnych zachowań programu. W tym celu
konieczne jest podzielenie wszystkich możliwych wartości danych wejściowych na
klasy wartości przetwarzanych podobnie (w sensie jakiegoś kryterium) i wybranie do
testów pewnej liczby wartości należących do każdej z tych klas. Do najczęściej
stosowanych metod deterministycznego projektowania testów należą:
analiza klas równoważności
analiza wartości brzegowych.
Wraz z rozwojem metod obiektowych pojawiły się te: metody odwołujące się do
specyfikacji przypadków użycia. Wymienione metody nie są ze sobą sprzeczne,
dlatego w praktyce stosuje się często kombinację różnych metod.
Analiza klas równoważności
Analiza klasy równoważności (equivalence partitioning) ma na celu podział
wszystkich możliwych wartości danych wejściowych programu na klasy (zbiory)
przetwarzane w podobny sposób. Uzasadnieniem metody jest założenie, że różne
defekt) programu są związane z różnymi sposobami przetwarzania. Projektowanie
testów rozpoczyna się od analizy specyfikacji testowanego programu i podziału
wszystkich możliwych wartości danych wejściowych na klasy równoważności,
zawierające wartości przetwarzane w taki sam sposób. W kolejnym kroku następuje
wybranie pewnej liczby wartości danych testowych z każdej klasy. W całym procesie
projektowania przypadków testowych należy uwzględnić zarówno klasy
odpowiadające poprawnym wartościom danych wejściowych, jak i klasy
odpowiadające wartościom błędnym. Liczba przypadków testowych dobieranych z
różnych klas równoważności decyduje o wiarygodności testowania.
Rysunek 9. Klasy równoważności wartości czasu
Na przykład, projektując testy komponentu, którego dane wejściowe obejmują
określenie czasu, można zauważyć, że prawidłowe wartości liczby godzin leżą w
zakresie 0-23, a liczby minut - w zakresie 0-59. Przyjmując ten podział, można
wyróżnić dziewięć klas równoważności (rys. 9). Projekt testów powinien objąć
wartości danych wejściowych z każdej z tych dziewięciu klas równoważności.
Analiza wartości brzegowych
Analiza wartości brzegowych (boundary value analysis) ma na celu znalezienie
wszystkich osobliwości leżących w całym zakresie danych wejściowych, jakie mogą
pojawić się na wejściu programu. Uzasadnieniem metody jest założenie, że błędy
mogą być powodowane nieprzewidzianym zachowaniem programu w okolicy wartości
granicznych. W związku z tym dla każdej znalezionej wartości granicznej przyjmuje
się jako przypadki testowe trzy wartości danych: wartość graniczną leżącą wewnątrz
obszaru wartości prawidłowych oraz wartość o jeden mniejszą i o jeden większą od
wartości granicznej.
Naturalnymi wartościami brzegowymi są granice klas równoważności. Stosując tę
metodę, trzeba jednak uwzględnić także granice innego rodzaju, wynikające z
właściwości użytych w programie typów danych. Na przykład, jeżeli temperatura w
oprogramowaniu sterującym klimatyzacją budynku jest przechowywana jako wartość
zmiennopozycyjna (typu
float lub real), to jako wartości brzegowe należy też przyjąć
wartość zero oraz wartości leżące na obu krańcach zakresu. Testowanie wartości
brzegowej powinno objąć trzy wartości różniące się o jeden bit w maszynowej
reprezentacji liczby. Warto zauważyć, że w tym miejscu wykracza się nieco poza
zakres podejścia czarnej skrzynki i korzysta z pewnych danych na temat sposobu
reprezentacji liczb w programie.
Testowanie sposobów użycia
Testowanie sposobów użycia oprogramowania (use case testing) ma na celu
znalezienie i przetestowanie wszystkich dostępnych dla użytkownika sposobów
wykorzystania programu. Uzasadnieniem tej metody jest założenie, że podobne
wykorzystanie programu ujawnia podobne błędy. Szczegóły metody zależą od postaci
specyfikacji programu. Jeżeli ma ona postać hierarchii funkcji, to sposobem
wykorzystania programu może być wykonanie funkcji lub pewnej sekwencji funkcji
tworzących jakąś biznesową całość. Jeżeli specyfikacja ma postać modelu
przypadków użycia, to sposobem wykorzystania jest każdy scenariusz przypadku
użycia. Kolejne przypadki testowe, projektowane za pomocą tej metody, powinny
więc zawierać takie wartości danych wejściowych, które spowodują wykonanie
wszystkich funkcji lub wszystkich scenariuszy.
Metoda testowania sposobów użycia jest szczególnie często stosowana podczas
projektowania testów akceptacyjnych oprogramowania wyspecyfikowanego za
pomocą metody przypadków użycia. Wiarygodność testowania będzie tym większa,
im większa będzie liczba przypadków testowych sprawdzających wykonanie
poszczególnych scenariuszy.
Testowanie losowe
Losowa metoda projektowania przypadków testowych (Monte Carlo method)
opiera się na założeniu, że przypadkowo generowane dane testowe rozłożą się w
całym możliwym zakresie wartości danych wejściowych i po dostatecznie dużej liczbie
prób pokryją cały zakres przetwarzania testowanego programu. Moment zakończenia
testów może być ustalony na podstawie pomiaru metryk pokrycia (klas
równoważności lub metryk opisanych w punkcie 3.2) albo - jak bywa najczęściej -
może być określony arbitralnie w harmonogramie projektu.
Środowisko losowego testowania programu składa się ze standardowego
generatora liczb losowych (np. funkcja biblioteczna
rand), filtru przetwarzającego
liczby losowe na dane leżące w zakresie wejściowym testowanego programu oraz z
automatu rejestrującego wynik (rys. 10). Zaletami losowej generacji danych testowych
są uproszczenie etapu projektowania przypadków testowych oraz łatwość
automatyzacji. Do jej wad należy zaliczyć skomplikowany proces analizy
zarejestrowanych wyników oraz brak gwarancji osiągnięcia zadowalającego pokrycia
testów w założonym czasie.
Rysunek 10. Środowisko losowej generacji danych testowych
4.2. Testowanie białej skrzynki
Testowanie metodą białej (szklanej) skrzynki jest próbą sprawdzenia poprawności
wszystkich elementów, z których zbudowany jest program. Projektowanie
przypadków testowych polega na wybraniu takiego zbioru wartości danych
wejściowych, który zapewni pełne pokrycie kodu programu, zgodnie z przyjętym
kryterium oceny kompletności, np. z wykorzystaniem metryk pokrycia kodu,
opisanych w punkcie 3.1. Istotną wadą metody jest jej zależność od wewnętrznej
struktury programu - każda zmiana tej struktury może wymagać modyfikacji zbioru
przypadków testowych. Metoda może być stosowana na wszystkich poziomach
testowania z wyjątkiem testowania akceptacyjnego. W praktyce jednak głównym
obszarem jej zastosowania jest testowanie jednostkowe.
Testowanie ścieżek
Testowanie ścieżek (path testing) ma na celu sprawdzenie poprawności działania
wszystkich instrukcji wchodzących w skład programu. Uzasadnieniem metody jest
założenie, że źródłem błędów są defekty tkwiące w instrukcjach programu.
Sprawdzenie wszystkich instrukcji powinno więc pozwolić na ujawnienie wszystkich
defektów. Kluczowe znaczenie przy projektowaniu przypadków testowych ma
rozmieszczenie instrukcji warunkowych, które decydują o wyborze ścieżki obliczeń i
wykonaniu leżących na tej ścieżce bloków instrukcji. Ocenę kompletności testowania
umożliwiają metryki pokrycia kodu. Obliczenie tych metryk podczas trwania testów
wymaga zarejestrowania przejścia wykonania programu przez wszystkie wyjścia
instrukcji warunkowych (decyzji). Można w tym celu wykorzystać pułapki programu
uruchomieniowego (debugger).
Przygotowanie testów wymaga znalezienia - podczas statycznej analizy kodu -
wszystkich rozgałęzień programu, a następnie umieszczenia w punktach wyjścia
pułapek powodujących przekazanie sterowania do programu uruchomieniowego.
Czynność tę nazywa się czasem instrumentacją testowanego programu. Tak
przygotowany program może być wielokrotnie wykonywany w środowisku testowym z
różnymi wartościami danych wejściowych. Każde zadziałanie pułapki jest
rejestrowane, po czym wykonanie programu jest wznawiane i przebiega dalej aż do
osiągnięcia końca.
Wartości danych testowych, dostarczane podczas kolejnych wywołań programu,
mogą być wybierane przez testera, np. na podstawie analizy warunków rozgałęzień,
albo też mogą być generowane losowo. Koniec testowania wyznacza osiągnięcie
pełnego pokrycia programu zgodnie z wybraną metryką - pokrycia bloków, decyzji lub
dróg programu. Wyniki zarejestrowane dla wszystkich wykonanych przypadków
testowych są porównywane z wartościami obliczonymi na podstawie specyfikacji
programu. Jeśli wszystkie obliczone wyniki są zgodne z oczekiwaniami, to testowany
program jest uznawany za poprawny.
Poważnym problemem tej metody testowania jest duża liczba przypadków
testowych, wykonywanych podczas testów, i wynikający z tej liczby duży rozmiar
danych, które muszą być poddane analizie po zakończeniu testów.
Testowanie mutacyjne
Testowanie mutacyjne (mutation testing) ma na celu identyfikację skutecznych
przypadków testowych oraz ograniczenie rozmiaru danych rejestrowanych podczas
testów i analizowanych po ich zakończeniu, przez odrzucenie wyników przypadków
testowych uznanych za nieskuteczne. Rozróżnienie skutecznych i nieskutecznych
przypadków testowych następuje na podstawie wyników wykonania tego samego
testu dla dwóch wersji programu - oryginalnej i zmutowanej przez celowe
wprowadzenie pewnej liczby defektów. Uzasadnieniem metody jest założenie, że
defekty naturalne są wykrywane równie skutecznie jak defekty wprowadzone
sztucznie. Przypadek testowy, który wykrywa błąd sztuczny - tzn. taki, którego wynik
jest inny w programie oryginalnym niż w zmutowanym - jest uznawany za skuteczny.
Przypadek testowy, który nie wykrywa błędu i nie potrafi odróżnić mutanta, jest
uznawany za nieskuteczny.
Przygotowanie testów mutacyjnych nie wymaga statycznej analizy programu.
Mutacje mogą być generowane losowo (np. przez losową zmianę znaków w tekście
programu), z ograniczeniem do zmian zgodnych ze składnią języka programowania.
Dla zwiększenia skuteczności metody zwykle generuje się dużą liczbę zmutowanych
wersji programu (mutantów). Wszystkie mutanty są kompilowane i wykonywane
równolegle z oryginalnym programem na tych samych danych testowych. Wyniki
uzyskane przez testowany program są porównywane z wynikami mutantów. Jeżeli
wszystkie wyniki są takie same, to przypadek testowy zostaje uznany za nieskuteczny
i jego wyniki są ignorowane. Jeśli wynik testowanego programu różni się od wyniku
choćby jednego mutanta, to przypadek testowy zostaje uznany za skuteczny, a wynik
obliczony przez testowany program jest rejestrowany do późniejszej analizy. Wyniki
uzyskane w wyniku wykonania mutantów nie mają znaczenia i nie podlegają
rejestracji. Schemat blokowy ilustrujący zasadę testowania mutacyjnego jest
pokazany na rys. 11.
Rysunek 11. Środowisko do testowania mutacyjnego
Zaletą testowania mutacyjnego jest łatwość automatyzacji testów. Wadą jest
ograniczenie mutacji sterujących wyborem rejestrowanych przypadków testowych do
takich defektów, które mogą być wynikiem drobnych pomyłek programisty. W ten
sposób zbiór danych testowych zostaje niejawnie skorelowany z postacią
testowanego programu, który może zawierać też błędy grube. Do wad metody można
także zaliczyć konieczność posiadania automatycznych narzędzi wspomagających
oraz bardzo wydajnego środowiska testowego, które musi być zdolne do
równoległego wykonania programu i wszystkich jego mutantów.
5. Automatyzacja testowania
Podczas testowania, np. nowej jednostki programu, ten sam scenariusz
przypadku testowego jest wykonywany wielokrotnie dla różnych wartości danych
testowych. Jeżeli przetestowana jednostka jest wielokrotnie modyfikowana, np. w
kolejnych iteracjach procesu wytwórczego, to te same testy są wykonywane
wielokrotnie w ramach testów regresji. Testowanie jest czynnością powtarzalną, a
jednocześnie pracochłonną. Powtarzalność wskazuje, że automatyzacja testowania
jest możliwa. Pracochłonność sugeruje, że automatyzacja może przynieść wymierne
korzyści ekonomiczne. Dlatego zainteresowanie przemysłu informatycznego
rozwojem narzędzi wspomagających testowanie stale wzrasta.
Ponieważ testowanie jest procesem złożonym, można rozważać narzędzia
wspomagające różne etapy tego procesu: projektowanie testów, wykonanie i analizę
wyników. Innego wspomagania wymaga testowanie jednostek programu metodą
białej skrzynki, innego testowanie aplikacji na poziomie funkcji interfejsu użytkownika,
a jeszcze innego testowanie wydajności oprogramowania. Dlatego trudno jest podać
spójną klasyfikację narzędzi wspomagających testowanie. Na rynku istnieje ich wiele,
poczynając od komercyjnych systemów łączących narzędzia różnego rodzaju, a
kończąc na darmowych narzędziach dedykowanych do określonych zadań.
Narzędzia elementarne - sterowniki i namiastki
Elementarnym warunkiem wykonania testu jest możliwość uruchomienia i
wykonania testowanego programu. Jeżeli przedmiotem testowania jest jakiś
niesamodzielny moduł programu, np. klasa Rewers z punktu 4.4, zawierająca metodę
wypozycz( ):
public int wypozyczCWolumin wolumin)
{
if ( pozycja != wolumin.pozycja ) return ERROR;
this.wolumin = wolumin;
okresWypozyczenia = pozycja.obliczOkres(czytelnik.status);
return OK;
}
to wywołanie tej metody z właściwym argumentem wymaga napisania programu
głównego, który tę metodę wywoła. Taki program główny - napisany specjalnie na
potrzeby testowania - jest nazywany sterownikiem testowym (test driver). Ponieważ
testowana metoda
wypożycz() wywołuje podczas wykonania metodę obliczOkres( ) z
innej klasy, która być może nie została jeszcze opracowana lub która jest testowana
równolegle przez kogoś innego, to wykonanie testowanej metody wymaga dołączenia
do niej jakiejś funkcji, która zastąpi brakującą metodę. Taki program symulujący
podczas testów zachowanie brakującego komponentu jest nazywany namiastką (test
stub).
Sterowniki i namiastki są niezbędne w każdym procesie testowym, również
przeprowadzanym ręcznie. Dlatego nie są one uważane za narzędzia automatyzacji
testowania. Z drugiej strony zarówno sterownik testowy, jak i namiastka mogą być
bardzo proste albo bardzo złożone. Prosty sterownik może odczytać argument z
klawiatury, wywołać testowaną funkcję i wyświetlić wynik. Złożony sterownik testowy
może wielokrotnie wywoływać wykonanie testowanego programu z różnymi
argumentami, zapisanymi na liście lub pobieranymi z pliku, i rejestrować wyniki w
innym pliku. Prosta namiastka może być funkcją niemal pustą, zwracającą ustaloną
wartość. Złożona namiastka może symulować funkcjonalność brakującego
komponentu albo zwracać różne wartości, generowane losowo lub pobierane z pliku.
Zestaw złożony z takiego rozbudowanego sterownika, niezbędnych namiastek oraz
pliku danych testowych może być używany wielokrotnie, np. podczas testów regresji,
automatycznie realizując testy i generując za każdym razem kompletny plik z
zarejestrowanym wynikiem.
Automatyzacja testów jednostkowych
Testowanie jednostek programu jest procesem masowym - dotyczy wszystkich
opracowanych klas programu - i wielokrotnie powtarzanym podczas testów regresji.
Dlatego ten obszar testów stal się polem intensywnego rozwoju narzędzi
automatycznego testowania. Wzorcem, który wyznaczył kierunek rozwoju
automatyzacji testów jednostkowych, stała się darmowa biblioteka JUnit, opracowana
do testowania programów napisanych w języku Java. Sukces tej biblioteki
doprowadził do powstania podobnych narzędzi wspomagających tworzenie testów
programów w innych językach, takich jak CPPUnit dla języka C++, DUnit dla Delphi
(obiektowa wersja języka Pascal), NUnit dla C# lub PHPUnit i PerlUnit dla
popularnych języków skryptowych.
W celu zilustrowania sposobu użycia biblioteki JUnit rozważmy proces
przygotowania testu metody obliczOkres( ) klasy Ograniczona z punktu 4.4:
public class Ograniczona extends Pozycja
{
public int obliczOkres(int status)
//reguła RB2
{
switch ( status ) {
case PRACOWNIK: return 30;
case DOKTORANT: return 7;
default: return 0;
}
}
}
Przeprowadzenie testu wymaga utworzenia obiektu testowanej klasy, wywołania
metody i porównania wyniku z oczekiwaniami. Wszystkie te czynności należy zawrzeć
w klasie testu, która na mocy umowy - ale tylko umowy - nosi zazwyczaj nazwę równą
nazwie testowanej klasy z przyrostkiem Test:
import org.junit.* ;
import static org.junit.Assert.* ;
public class OgraniczonaTest {
@Test
public void test_oblicz0kres() {
Pozycja p = new Ograniczona(...) ;
assertTrue(p.oblicz0kres(PRACOWNIK)== 30);
assertTrue(p.oblicz0kres(DOKTORANT)== 7);
assertTrue(p.oblicz0kres(STUDENT) == 0);
}
}
Zasadniczym elementem utworzonej klasy jest metoda pełniąca rolę testu - w tym
przykładzie jest to metoda
test_obliczOkres( ) - wyróżniona za pomocą adnotacji
@Test. Obecność tej adnotacji umożliwi automatyczne odszukanie i wywołanie
metody przez sterownik testowy. Poza metodami testowymi utworzona klasa może
zawierać dowolne inne metody pomocnicze. Inne adnotacje pozwalają na wskazanie
metod, które powinny być wywołane przed lub po wywołaniu metody testowej.
Pierwszym elementem metody testu jest utworzenie obiektu klasy
Ograniczona,
którego zachowanie będzie przedmiotem weryfikacji. Następnie wywoływana jest
metoda
assertTrue( ), wchodząca w skład biblioteki JUnit, która sprawdza, czy
podane jako argument wyrażenie jest prawdziwe. Istnieje kilka metod assert...
różniących się postacią sprawdzanego warunku.
Przygotowane klasy testowe mogą być dołączone do testowanego programu,
skompilowane i wykonane za pomocą dołączonego do biblioteki sterownika (JUnit
test runner) na konsoli tekstowej lub za pomocą licznych środowisk graficznych. W
każdym przypadku wynikiem wykonania metody assert... może być komunikat
informujący o ewentualnym błędzie.
Biblioteka JUnit jest bardzo rozbudowana. Zawiera m.in. mechanizmy grupowania
testów w zestawy, sprawdzania wyjątków, specyfikowania wspólnych czynności
przygotowawczych i zamykających oraz współpracy ze zbiorami danych. Pełne
przedstawienie możliwości tego narzędzia przekracza ramy.
Narzędzia pokrycia kodu
Ręczne obliczanie metryk pokrycia kodu (punkt 3.1) podczas testów jest
uciążliwe. Dlatego istnieje cały szereg narzędzi pokrycia kodu (code coverage tools),
zarówno komercyjnych, jak i darmowych, służących automatyzacji tego zadania.
Działanie takich narzędzi obejmuje zwykle trzy kroki.
1. Statyczną analizę kodu programu i zaznaczenie wszystkich rozgałęzień.
2. Instrumentację kodu, polegającą na dołączeniu funkcji rejestrujących
przejście wykonania programu przez każde rozgałęzienie.
3. Rejestrację śladu wykonania programu (podczas testów) i obliczenie
wartości metryk.
Niektóre narzędzia współpracują bezpośrednio z biblioteką JUnit (lub jej wersjami
dla innych języków programowania) i umożliwiają ocenę pokrycia kodu przez
konkretny zestaw testów.
Odtwarzanie działań użytkownika
Testowanie funkcji oprogramowania metodą czarnej skrzynki polega na
wielokrotnym wykonaniu testowanego komponentu dla różnych danych testowych
wprowadzanych za pośrednictwem jego interfejsu. W przypadku testowania
kompletnej aplikacji jest to interfejs użytkownika. Jeżeli program ma interfejs
tekstowy, to wykonanie testów łatwo zautomatyzować za pomocą sterownika
testowego, który przekieruje wejściowy strumień programu z klawiatury do pliku
danych testowych, a strumień wyjściowy z ekranu do pliku wyników. Jeżeli testowana
aplikacja ma interfejs graficzny, to automatyzacja testów wymaga innego podejścia.
Narzędziem automatyzacji testów aplikacji wyposażonej w graficzny interfejs
użytkownika jest zmodyfikowana biblioteka operacji okienkowych, która po
rekompilacji i uruchomieniu programu przechwytuje i zapamiętuje działania operatora,
takie jak kliknięcia przycisków, ruchy myszką i wypełnienie pól tekstowych. Pierwszy
przebieg testów jest obsługiwany interaktywnie przez człowieka, którego ruchy oraz
wyniki działań są zapamiętywane w pliku. Po zakończeniu wykonania możliwe jest
automatyczne, wielokrotne odtwarzanie wszystkich zapamiętanych działań,
połączone z porównaniem zgodności wyników.
Zastosowanie narzędzia odtwarzającego działania użytkownika (record and
playback feature) może przynieść wymierne korzyści w postaci automatycznego
wykonywania testów oprogramowania rozwijanego w sposób iteracyjny lub
oprogramowania po modyfikacji. W tym ostatnim przypadku wadą metody jest jednak
wrażliwość na wszelkie zmiany dokonywane w interfejsie użytkownika.
Technika odtwarzania działań użytkownika, połączona z zupełnie innym
mechanizmem realizacyjnym, umożliwia też automatyzację testowania aplikacji
internetowych. W tym przypadku jednak przedmiotem przechwytywania i
porównywania nie są działania użytkownika, tylko wymieniane z użytkownikiem strony
html. Zaletą metody jest brak konieczności ingerowania w kod aplikacji.
Testowanie wydajności
Testowanie wydajności oprogramowania działającego w trybie wsadowym, np.
oprogramowania naliczającego dopłaty rolne na podstawie wcześniej
wprowadzonych wniosków, nie wymaga stosowania automatycznych narzędzi innych
od generatora zawartości testowej bazy danych. Problem pojawia się podczas
testowania systemów interaktywnych, np. oprogramowania systemu bankowego,
które powinny obsługiwać wielu (tysiące) użytkowników jednocześnie. Wykorzystanie
kilku tysięcy testerów, jakkolwiek teoretycznie możliwe, byłoby drogie i
nieprzewidywalne - raz zarejestrowanego wyniku nie dałoby się powtórzyć po raz
drugi dokładnie w ten sam sposób.
Rozwiązaniem problemu jest użycie tzw. robota testowego (test robot), czyli
oprogramowania symulującego działanie dużej liczby użytkowników. Aby obniżyć
koszty, robot nie wykorzystuje zwykle graficznego interfejsu użytkownika, lecz
przesyła dane wejściowe, np. polecenia przelewu z konta na konto w systemie
bankowym, poprzez interfejs najłatwiej dostępny. Na przykład, jeżeli testowane
oprogramowanie działa na serwerze komunikującym się ze stacjami roboczymi
użytkowników poprzez sieć, za pomocą protokołu HTTPS, to robot generuje
obciążenie w postaci sekwencji pakietów tego protokołu. Jeżeli natomiast testowaniu
podlega komponent EJB komunikujący się z otoczeniem poprzez kolejki JMS (Java
Message Service), to robot generuje serie komunikatów JMS.
Robot generujący obciążenie dla konkretnej aplikacji i konkretnego protokołu
komunikacyjnego jest narzędziem prostym. Drugą częścią problemu jest jednak
ocena wyników testowania. Niekiedy wystarczy rejestrowanie strumieni danych
wejściowych i wyjściowych, za pomocą standardowych narzędzi administratora
systemu. W takim przypadku ocena wyników wymaga późniejszej analizy treści
dziennika. Czasami wystarczy sam pomiar obciążenia systemu - użycia procesora,
zajętości pamięci - obserwowany za pomocą narzędzi administratora w czasie trwania
testu. Bardziej zaawansowanym sposobem jest automatyczna analiza wyników
przetwarzania przez odpowiednio zaprogramowanego robota, który może np. mierzyć
czas odpowiedzi na wysiane do testowanej aplikacji żądanie (rys. 12).
Rysunek 12. Robot testowy
Jeszcze inny sposób prowadzenia testów umożliwia połączenie testów
wydajnościowych z testami funkcjonalnymi. W tym podejściu robot wytwarza
obciążenie, którego poziom jest kontrolowany za pomocą narzędzi administratora.
Wyniki przetwarzania danych dostarczanych przez robota nie są rejestrowane. Na tle
tego obciążenia wykonuje się ręcznie testy funkcjonalne, których wyniki weryfikują
poprawność działania oprogramowania przy ustalonym obciążeniu systemu. Takie
podejście jest często stosowane podczas testowania akceptacyjnego.
6. Uwagi bibliograficzne
Przegląd dziedziny testowania programów komputerowych można znaleźć w
książkach [90, 92]. Szersze ujęcie problemu, obejmujące także testowanie układów i
zespołów cyfrowych systemu komputerowego, jest podane w [91].
Historia. Testowanie jest nieodłącznym elementem tworzenia programów.
Systematyczne podejście do badania kompletności testowania za pomocą metryk
pokrycia datuje się od pracy [99]. Główne ograniczenie testowania, którym jest brak
możliwości udowodnienia poprawności programu, wyartykułowane w informatyce
przez Dijkstrę [217], wynika wprost z ograniczeń testowania sformułowanych przez
Poppera [258] w kontekście falsyfikowania teorii naukowych. Inspekcje kodu (por.
punkt 7.3.2) są związane z nazwiskiem Fagana [97, 98]. Zgrabne i wciąż cytowane
zdanie, definiujące czynności weryfikacji i zatwierdzania (walidacji): „Vérification is
building the product right, and Validation is building the right producf\ pochodzi od
Boehma [96].
Metryki. Obszerny przegląd różnych metryk oraz modeli efektywności usuwania
defektów oprogramowania można znaleźć w [114].
Biblioteka JUnit. Testowanie jednostkowe programów napisanych w Javie za
pomocą biblioteki JUnit staje się powoli standardem. Podręcznik i darmową wersję
biblioteki można znaleźć w Internecie [253]. Dobrymi podręcznikami w tradycyjnej
formie są [89, 93]. Biblioteka JUnit wchodzi również w skład darmowego środowiska
Eclipse [250].
Systemy równoległe. W książce są pominięte zupełnie zaawansowane problemy
testowania systemów równoległych. Nie ma wielu książek podejmujących ten temat.
Zainteresowany czytelnik może sięgnąć do dwóch całkiem nowych pozycji [94, 95].