Zaawansowane CPP/Wykład 4: Testowanie: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Matiunreal (dyskusja | edycje)
m Zastępowanie tekstu - "<div class="thumb t(.*)"><div style="width:(.*);"> <flash>file=(.*)\.swf\|width=(.*)\|height=(.*)<\/flash> <div\.thumbcaption>(.*)<\/div> <\/div><\/div>" na "$4x$5px|thumb|$1|$6"
 
(Nie pokazano 44 wersji utworzonych przez 7 użytkowników)
Linia 1: Linia 1:
==Wstęp==
==Wstęp==


Programowanie rozumianie jako pisanie kodu, jest tylko częścią procesu
Programowanie rozumiane jako pisanie kodu jest tylko częścią procesu
tworzenia oprogramowania. Analiza i opis tego procesu jest przedmiotem
tworzenia oprogramowania. Analiza i opis tego procesu jest przedmiotem
inżynierii oprogramowania i znacznie wykracza poza ramy tego wykładu.
inżynierii oprogramowania i znacznie wykracza poza ramy tego wykładu.
Niemniej chciałbym po krótce w tym wykładzie poruszyć jedno zagadnienie
Niemniej chciałbym pokrótce w tym wykładzie poruszyć jedno zagadnienie
związane bezpośrednio z programowaniem: testowanie.  
związane bezpośrednio z programowaniem - testowanie.  
Testowanie jest nieodłączną częścią programowania i
Testowanie jest nieodłączną częścią programowania i
powinno być obowiązkiem każdego programisty. Jak będę się starał
powinno być obowiązkiem każdego programisty. Jak będę się starał
państwa przekonać testowanie to może być coś więcej niż "tylko"
Państwa przekonać, testowanie to może być coś więcej niż "tylko"
sprawdzenie poprawności kodu.
sprawdzenie poprawności kodu.


Linia 14: Linia 14:


Testowanie wydaje się oczywistą koniecznością w przypadku każdego
Testowanie wydaje się oczywistą koniecznością w przypadku każdego
programu komputerowego (choć co rok zdaża mi się spotkać studentów
programu komputerowego (choć co rok zdarza mi się spotkać studentów
przekonanych o swojej nieomylności:). Mniej oczywiste jest
przekonanych o swojej nieomylności :)). Mniej oczywiste jest
stwierdzenie kto, gdzie, kiedy i co ma testować. To w ogólności bardzo
stwierdzenie kto, gdzie, kiedy i co ma testować. To w ogólności bardzo
złożony problem, ale tu chciałbym się ograniczyć do tzw. testów
złożony problem, ale tu chciałbym się ograniczyć do tzw. testów
jednostkowych. Wyrażenie "test jednostkowy" należy interpretować
jednostkowych. Wyrażenie "test jednostkowy" należy interpretować
jako test jednej jednostki. Przez pojedynczą jednostkę będziemy
jako test jednej jednostki. Przez pojedynczą jednostkę będziemy
rozumieli, funkcję, metodę lub klasę. Zadaniem takiego testu jest
rozumieli: funkcję, metodę lub klasę. Zadaniem takiego testu jest
sprawdzenie czy dana jednostka działa poprawnie.  
sprawdzenie czy dana jednostka działa poprawnie.  


Dlaczego w ogóle pisać takie testy? Czy nie wystarczy przetestowanie
Dlaczego w ogóle pisać takie testy? Czy nie wystarczy przetestowanie
całego programu? Testować cały program, też oczywiście trzeba. Służą
całego programu? Testować cały program też oczywiście trzeba. Służą
do tego liczne testy odbioru, integracyjne itp. wykonywane poprzez
do tego liczne testy odbioru, integracyjne itp., wykonywane poprzez
dedykowane zespoły.  Ale im większą część kodu testujemy, tym
dedykowane zespoły.  Ale im większą część kodu testujemy, tym
trudniejsze są testy i tym trudnie będzie znaleźć przyczynę wykrytej
trudniejsze są testy i tym trudniej będzie znaleźć przyczynę wykrytej
nieprawidłowości działania programu. Testy jednostkowe wykrywają
nieprawidłowości działania programu. Testy jednostkowe wykrywają
błędy (a przynajmniej ich część) "u źródła", często w bardzo prostym
błędy (a przynajmniej ich część) "u źródła", często w bardzo prostym
kodzie a więc ich poprawianie  może być dużo szybsze.  
kodzie, a więc ich poprawianie  może być dużo szybsze.  


Jak się zastanowić to testowanie każdej wykonanej jednostki przed
Jak się zastanowić, to testowanie każdej wykonanej jednostki przed
użyciem jej w dalszym kodzie powinno być oczywistą koniecznością. No,
użyciem jej w dalszym kodzie powinno być oczywistą koniecznością. No,
ale równie oczywiste jest że należy uprawiać sporty, nie palić
ale równie oczywiste jest, że należy uprawiać sporty, nie palić
paoierosów, nie jeździć po pijanemu, itp. Statystyki dobitnie jednak
papierosów, nie jeździć po pijanemu, itp. Statystyki dobitnie jednak
pokazują, że natura człowiecza grzeszną jest i łatwo ulegamy
pokazują, że natura człowiecza grzeszną jest i łatwo ulegamy
słabością, w tym wypadku pokusie nie testowanie programów, a powodem
słabościom, w tym wypadku pokusie nietestowania programów, a powodem
jest jeden z siedmiu grzechów głównych czyli lenistwo. Testy nie
jest jeden z siedmiu grzechów głównych czyli lenistwo. Testy nie
piszą, ani nie wykonuja się same, w rzeczywistości wymagają całkiem
piszą, ani nie wykonuja się same, w rzeczywistości wymagają całkiem
sporego nakładu pracy (czasami większego niż pisanie testowanego
sporego nakładu pracy (czasami większego niż pisanie testowanego
kodu!). Część programistów traktuje ten wysiłek jako "czas
kodu!). Część programistów traktuje ten wysiłek jako "czas
stracony", tzn. nie wykorzystany na pisanie kodu za który im płacą.
stracony", tzn. nie wykorzystany na pisanie kodu, za który im płacą.


To wszystko jest prawdą, ale tak naprawdę nie możemy uniknąć tej
To wszystko jest prawdą, ale w rzeczywistości nie możemy uniknąć tej
straty czasu. Jest to tylko kwestia wyboru, gdzie i ile tego czasu
straty czasu. Jest to tylko kwestia wyboru gdzie i ile tego czasu
stracimy: czy na pisanie testów w trakcie kodowanie jednostek i
stracimy: czy na pisanie testów w trakcie kodowania jednostek i
poprawianie prostych błędów, czy na szukanie błędów w dużo większych
poprawianie prostych błędów, czy na szukanie błędów w dużo większych
fragmentach programu? Doświadczenie wykazuje że czas potrzebny na
fragmentach programu? Doświadczenie wykazuje, że czas potrzebny na
znalezienie i poprawienie błedu jest w tym drugim przypadku dużo
znalezienie i poprawienie błędu jest w tym drugim przypadku dużo
większy, w krańcowych przypadkach poprawienie programu może być wręcz
większy, w krańcowych przypadkach poprawienie programu może być wręcz
niemożliwe.  Testy jednostkowe skalują się liniowo z rozmiarem
niemożliwe.  Testy jednostkowe skalują się liniowo z rozmiarem
Linia 59: Linia 59:
Jak już napisałem testy służą do sprawdzania poprawności działania
Jak już napisałem testy służą do sprawdzania poprawności działania
danej jednostki: '''ciągłego''' sprawdzanie działania tej jednostki.
danej jednostki: '''ciągłego''' sprawdzanie działania tej jednostki.
Ponieważ kod się nieustanie zmienia powinnismy wykonywać testy cały
Ponieważ kod się nieustannie zmienia, powinniśmy wykonywać testy cały
czas aby sprawdzić czy te zmiany wynikające np. z poprawienia
czas aby sprawdzić czy te zmiany wynikające, np. z poprawienia
wykrytych błędów nie wprowadziły nowych usterek. Aby to było możliwe
wykrytych błędów, nie wprowadziły nowych usterek. Aby to było możliwe
testy musza być łatwe do wykonywania, czyli zautomatyzowane. Nie mogą
testy musza być łatwe do wykonywania czyli zautomatyzowane. Nie mogą
polegać na tym że puszczamy program a następnie przeglądamy wydruk.
polegać na tym, że puszczamy program, a następnie przeglądamy wydruk.
Musimy "nacisnąć guzik" a w odpowiedzi zapali  nam się szereg
Musimy "nacisnąć guzik" a w odpowiedzi zapali  nam się szereg
"swiatełek": zielone wskażą testy zaliczone, a czerwone testy nie
"światełek": zielone wskażą testy zaliczone, a czerwone testy niezaliczone. Taki automatyczny proces testujący może być zintegrowany z
zaliczone. Taki automatyczny proces testujący może być zintegrowany z
innymi narzędzimi programistycznymi, takimi jak np. <tt>make</tt>.
innymi narzędzimi programistycznymi takimi jak np. {make}.


==Refaktoryzacja==
===Refaktoryzacja===


Wysiłek włożony w napisanie takich powtarzalnych testów nie jest
Wysiłek włożony w napisanie takich powtarzalnych testów nie jest
zaniedbywalny, ale korzyści są duże.  Możliwość przetestowania kodu w
zaniedbywalny, ale korzyści są duże.  Możliwość przetestowania kodu w
dowolnym momencie to więcej niż tylko świadomość że nasz obecny kod
dowolnym momencie to więcej niż tylko świadomość, że nasz obecny kod
przeszedł testy. Taka możliwość w połączeniu z narzędziami
przeszedł testy. Taka możliwość, w połączeniu z narzędziami
zarządzającymi kodem źródłowym, pozwala na bezpieczne dokonywanie
zarządzającymi kodem źródłowym, pozwala na bezpieczne dokonywanie
zmian, według schematu: zmieniamy, testujemy, jeśli testy się nie
zmian według schematu: zmieniamy, testujemy, jeśli testy się nie
powiodą szukamy błedów, jeśli ich nie znajdujemy to cofamy zmiany.
powiodą szukamy błedów, jeśli ich nie znajdujemy to cofamy zmiany.
Takie podejście jest szczególnie pomocne, a właściwie niezbędne
Takie podejście jest szczególnie pomocne, a właściwie niezbędne,
podczas programowania przyrostowego i refaktoryzacji.  
podczas programowania przyrostowego i refaktoryzacji.  


Programowanie przyrostowe to technika zalecena dla większości
Programowanie przyrostowe to technika zalecana dla większości
projektów, która polega na programowanie "malymi krokami", czyli na
projektów, która polega na programowaniu "malymi krokami", czyli na
kolejnym dodawaniu nowych funkcji do istniejącego działajacego kodu.
kolejnym dodawaniu nowych funkcji do istniejącego działajacego kodu.
Testy umożliwiają sprawdzenie czy dodany kod nie wprawdził błedów do
Testy umożliwiają sprawdzenie czy dodany kod nie wprowadził błędów do
starej części. Oczywiście każdy przyrost wymaga stworzenia nowego
starej części. Oczywiście każdy przyrost wymaga stworzenia nowego
zestawu testów.  
zestawu testów.  


Refaktoryzacja to zmiana kodu bez zmiany jego funkcjonalności, w celu
Refaktoryzacja to zmiana kodu bez zmiany jego funkcjonalności w celu
poprawy jego jakości.  
poprawy jego jakości.


==Projektowanie sterowane testami==
===Projektowanie sterowane testami===


Pisanie testu, sprawdza również nasze zrozumienie tego, co dana
Pisanie testu sprawdza również nasze zrozumienie tego, co dana
jednostka ma robić. W tym sensie testy stanowią sformalizowany zapis
jednostka ma robić. W tym sensie testy stanowią sformalizowany zapis
wymagań. To zaleta którą trudno przecenić, jeżeli nie wiemy jak
wymagań. To zaleta, którą trudno przecenić. Jeżeli nie wiemy jak
przestestować daną jednostkę to najprawdopodobniej nie powinnismy się
przestestować daną jednostkę, to najprawdopodobniej nie powinniśmy się
wcale brać za jej kodowanie.  Dlatego niektóre metodologie (np.
wcale brać za jej kodowanie.  Dlatego niektóre metodologie (np.
extreme programming) zalecają projektowanie sterowane testami, czyli
extreme programming) zalecają projektowanie sterowane testami, czyli
zaczęcie pisania programu od pisania testów do niego. Przebieg pracu
zaczęcie pisania programu od pisania testów do niego. Przebieg pracy
przy takim podejściu wygląda następująco:
przy takim podejściu wygląda następująco:
# Piszemy test
# Piszemy test
# Kompilujemy test
# Kompilujemy test
Linia 107: Linia 107:
# Test się kompiluje
# Test się kompiluje
# Wykonujemy test
# Wykonujemy test
# Test sie najprawdpodobnie nie wykonuje poprawnie
# Test najprawdpodobniej nie wykonuje się poprawnie
# Poprawiamy/dopisujemy  kod tak aby test się wykonał
# Poprawiamy/dopisujemy  kod tak aby test się wykonał
# Test się wykonuje poprawnie  
# Test wykonuje się poprawnie  


Osobiście wydaje mi się, że zalety tego podejścia są ogromne. Nawet,
Osobiście wydaje mi się, że zalety tego podejścia są ogromne. Nawet
jeśli brakuje czasu i ochoty na pisanie testów, to należy po prostu
jeśli brakuje czasu i ochoty na pisanie testów, to należy po prostu
zastanowić się nad sposobem przetestowania naszego kodu zanim
zastanowić się nad sposobem przetestowania naszego kodu zanim
Linia 117: Linia 117:
tego, co właściwie nasz program ma robić.
tego, co właściwie nasz program ma robić.


==Testy==
===Testy===


Po tej całej propagandzie czas na testowanie w praktyce, przetestujemy
Po tej całej propagandzie - czas na testowanie w praktyce. Przetestujemy
przykłady wprowadzone w poprzednich rozdziałach, zaczynając od funkcji
przykłady wprowadzone w poprzednich rozdziałach, zaczynając od funkcji
{max}:  
<tt>max</tt>:  


template<typename T> T max(T a,T b) {return (a>b)?a:b;}
template<typename T> T max(T a,T b) {return (a>b)?a:b;}


Być może część z państwa wybuchnie tu z oburzeniem, jak to ? testować
Być może część z Państwa oburzy się: jak to? testować
jedną linijkę? Gdyby chodziło o jedną linijkę to rzeczywiście
jedną linijkę? Gdyby chodziło o jedną linijkę to rzeczywiście
wystarczy na nią popatrzeć (i mieć wiarę w kompilator), ale
wystarczy na nią popatrzeć (i mieć wiarę w kompilator), ale
przypominam że dodalismy do niej dwie przeciążone wersje i dwie  
przypominam, że dodaliśmy do niej dwie przeciążone wersje i dwie  
specjalizacje. A to oznacza że ta linijka nie działa poprawnie w
specjalizacje. A to oznacza, że ta linijka nie działa poprawnie w
każdym przypadku.  Napiszemy więc testy które to wyłapią, jak już
każdym przypadku.  Napiszemy więc testy, które to wyłapią. Jak już
pisałem będzie to jednoczesne zdefiniowanie tego jak funkcja {max}
pisałem będzie to jednoczesne zdefiniowanie tego jak funkcja <tt>max</tt>
ma działać. Po chwili zastanowienia ustalamy więc że nasza funkcja
ma działać. Po chwili zastanowienia ustalamy więc, że nasza funkcja
{max} zwraca:  
<tt>max</tt> zwraca:  
* większą z dwu przekazanych wartości (większą w sensie porównania
* większą z dwu przekazanych wartości (większą w sensie porównania operatorem <tt>></tt>)
operatorem {>}).
* jeśli argumentami są wskaźniki to <tt>max</tt> zwraca wskaźnik na większą wartość
* jeśli argumentami są wskaźniki to {max} zwraca wskaźnik na
* jeśli argumentami są wskaźniki na <tt>char</tt> to <tt>max</tt> traktuje je jako napisy i zwraca wskaźnik do napisu większego zgodnie z uporządkowaniem leksykalnym.
większą wartość.
* jeśli argumentami są wskaźniki na {char} to {max}
traktuje je jako napisy i zwraca wskaźnik do napisu większego
zgodnie z uporządkowaniem leksykalnym.


Testować funkcję {max} będziemy poprzez porównanie wartości
Testować funkcję <tt>max</tt> będziemy poprzez porównanie wartości
zwracanej do wartości poprawnej która sami wskażemy:
zwracanej do wartości poprawnej, którą sami wskażemy:


assert(max(1,2)<nowiki>=</nowiki><nowiki>=</nowiki>2);
assert(max(1,2)<nowiki>=</nowiki><nowiki>=</nowiki>2);
assert(max(2,1)<nowiki>=</nowiki><nowiki>=</nowiki>2);
assert(max(2,1)<nowiki>=</nowiki><nowiki>=</nowiki>2);


Należy też sprawdzić przypadek symetryczny
Należy też sprawdzić przypadek symetryczny


assert(max(1,1)<nowiki>=</nowiki><nowiki>=</nowiki>1);
assert(max(1,1)<nowiki>=</nowiki><nowiki>=</nowiki>1);
assert(max(2,2)<nowiki>=</nowiki><nowiki>=</nowiki>2);
assert(max(2,2)<nowiki>=</nowiki><nowiki>=</nowiki>2);


Testowanie szablonu jest trudne bo w zasadzie musimy rozważyć
Testowanie szablonu jest trudne, bo w zasadzie musimy rozważyć
porzekazanie argumentów dowolnego typu. Co więcej ten typ wcale nie
przekazanie argumentów dowolnego typu. Co więcej, ten typ wcale nie
musi posiadać operatora porównania {<nowiki>=</nowiki><nowiki>=</nowiki>}. Aby sprawdzić działanie
musi posiadać operatora porównania <tt><nowiki>=</nowiki><nowiki>=</nowiki></tt>. Aby sprawdzić działanie <tt>max</tt> na jakimś typie niewbudowanym posłużymy się własną klasą:
{max} na jakimś typie niewbudowanym posłużymy się własną klasą:


class Int {
class Int {
private:
private:
int _val;
  int _val;
public:
public:
Int(int i):_val(i) {};
  Int(int i):_val(i) {};
int val()const {return _val;};
  int val()const {return _val;};
friend bool operator>(const Int &a,const Int &b)  {
  friend bool operator>(const Int &a,const Int &b)  {
return a._val>b._val;  
  return a._val>b._val;  
}
  }
};
};


Kod testowy może teraz wyglądać następująco:
Kod testowy może teraz wyglądać następująco:


Int i(5);
Int i(5);
Int j(6);
Int j(6);
assert(max(i,j).val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(i,j).val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(j,i).val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(j,i).val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(j,j).val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(j,j).val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(i,i).val() <nowiki>=</nowiki><nowiki>=</nowiki> 5);
assert(max(i,i).val() <nowiki>=</nowiki><nowiki>=</nowiki> 5);


Następnie testujemy wersję wskaźnikową:
Następnie testujemy wersję wskaźnikową:


assert(max(&i,&j)->val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(&i,&j)->val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(&j,&i)->val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(&j,&i)->val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(&j,&j)->val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(&j,&j)->val() <nowiki>=</nowiki><nowiki>=</nowiki> 6);
assert(max(&i,&i)->val() <nowiki>=</nowiki><nowiki>=</nowiki> 5);
assert(max(&i,&i)->val() <nowiki>=</nowiki><nowiki>=</nowiki> 5);


i na koniec wersję dla napisów ({const char *}):
i na koniec wersję dla napisów (<tt>const char *</tt>):


assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max("abcd","acde"),"acde"));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max("abcd","acde"),"acde"));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max("acde","abcd"),"acde"));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max("acde","abcd"),"acde"));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max("abcd","abcd"),"abcd"));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max("abcd","abcd"),"abcd"));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max("acde","acde"),"acde"));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max("acde","acde"),"acde"));


oraz ({char *}):
oraz (<tt>char *</tt>):


char s1[]<nowiki>=</nowiki>"abcde";
char s1[]<nowiki>=</nowiki>"abcde";
char s2[]<nowiki>=</nowiki>"ac";
char s2[]<nowiki>=</nowiki>"ac";
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max(s1,s2),s2));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max(s1,s2),s2));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max(s2,s1),s2));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max(s2,s1),s2));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max(s1,s1),s1));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max(s1,s1),s1));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max(s2,s2),s2));
assert(0<nowiki>=</nowiki><nowiki>=</nowiki>strcmp(max(s2,s2),s2));


Napisy zostały tak dobrane żeby miały pierwszy znak identyczny,
Napisy zostały tak dobrane, żeby miały pierwszy znak identyczny,
ponieważ procedura ogólna dla wskaźników porównuje właśnie pierwsze
ponieważ procedura ogólna dla wskaźników porównuje właśnie pierwsze
znaki. To pokazuje jak ważny jest wybór danych testowych. Testy
znaki. To pokazuje jak ważny jest wybór danych testowych. Testy
jednostkowe są testami "białej skrzynki" tzn. piszący testy ma wgląd
jednostkowe są testami "białej skrzynki", tzn. piszący testy ma wgląd
do implementacji testowanego kodu (powinien to być ten sam
do implementacji testowanego kodu (powinien to być ten sam
programista), można więc przygotować dane testowe tak, aby prokrywały
programista), można więc przygotować dane testowe tak, aby pokrywały
możliwie wiele ścieżek wykonywania programu, warunków brzegowych itp.  
możliwie wiele ścieżek wykonywania programu, warunków brzegowych itp.  


Oczywiście bezbłędne przejście powyższych testów, nie gwarantuje nam
Oczywiście bezbłędne przejście powyższych testów nie gwarantuje nam
jeszcze poprawności funkcji {max} (tego zresztą nie zagwarantuje
jeszcze poprawności funkcji <tt>max</tt> (tego zresztą nie zagwarantuje
nam żaden test), ale bardzo zwiększa prawdopodobieństwo tego że tak
nam żaden test), ale bardzo zwiększa prawdopodobieństwo tego, że tak
jest.
jest.


Pozostaje nam do wytestowanie procedura szukająca maksimum w tablicy.
Pozostaje nam do wytestowanie procedura szukająca maksimum w tablicy.
To zadanie zostawiam jako ćwiczenie dla czytelników.  
To zadanie zostawiam jako ćwiczenie dla czytelników.


====


Powyższy przykład może budzić pewne obawy. Kod funkcji {max} liczy
Powyższy przykład może budzić pewne obawy. Kod funkcji <tt>max</tt> liczy
około 20 lini, a kod testujący 40. No cóż, testowanie kosztuje, tylko
około 20 linii, a kod testujący 40. No cóż, testowanie kosztuje, tylko
czy możemy sobie pozwolić na zrezygnowanie z niego? Rozważmy powyższy
czy możemy sobie pozwolić na zrezygnowanie z niego? Rozważmy powyższy
przykład, te dwadzieścia  lini kodu definiującego szablon {max} jest dużo
przykład, te dwadzieścia  linii kodu definiującego szablon <tt>max</tt> jest dużo
bardziej skomplikowane niż te czterdzieści lini kodu testujacego, tak że jeśli
bardziej skomplikowane niż te czterdzieści linii kodu testujacego, tak że jeśli
porównamy czas pisania, to nie wypada to już tak źle. Kod szablonu
porównamy czas pisania, to nie wypada to już tak źle. Kod szablonu
{max} zawiera przeciążenia i specjalizację, czyli wykorzystany jest
<tt>max</tt> zawiera przeciążenia i specjalizację, czyli wykorzystany jest
algorytm rozstrzygania przeciążenia. Osobiście nie ufam swojej
algorytm rozstrzygania przeciążenia. Osobiście nie ufam swojej
znajomości tego algorytmu na tyle, aby nie sprawdzić kodu. Jeżeli i tak
znajomości tego algorytmu na tyle, aby nie sprawdzić kodu. Jeżeli i tak
wykonujemy jakieś testy to warto zainwestować trochę więcej pracy i
wykonujemy jakieś testy to warto zainwestować trochę więcej pracy i
przygotować porządny zestaw testujący.  Może też się zdarzyć że
przygotować porządny zestaw testujący.  Może też się zdarzyć, że
będziemy chcieli dodać kolejne przeciążenia, które mogą wpłynąć w
będziemy chcieli dodać kolejne przeciążenia, które mogą wpłynąć w
niezamierzony sposób na przeciążenia już istniejące. Gotowy programik
niezamierzony sposób na przeciążenia już istniejące. Gotowy programik
Linia 236: Linia 230:
Pozostaje jeszcze sprawa poprawności samego programu testującego, w
Pozostaje jeszcze sprawa poprawności samego programu testującego, w
końcu to też jest kod i może zawierać błędy. Czy więc musimy pisać kod
końcu to też jest kod i może zawierać błędy. Czy więc musimy pisać kod
testujący program testujacy, i tak dalej w nieskończoność? Na
testujący program testujący, i tak dalej w nieskończoność? Na
szczeście nie, błędy w kodzie testującym mogą mieć dwa efekty:
szczęście nie, błędy w kodzie testującym mogą mieć dwa efekty.
Pierwszy to wykazanie błedu który nie jest błedem, w tej sytuacji taki
Pierwszy to wykazanie błędu, który nie jest błędem, w tej sytuacji taki
błąd wykrywa się sam. Musimy tylko pamiętać podczas
błąd wykrywa się sam. Musimy tylko pamiętać podczas
szukania źródeł wykazanych przez program testujacy usterek, że mogą
szukania źródeł wykazanych przez program testujacy usterek, że mogą
one pochodzić z kodu testującego. Drugi rodzaj błedu to nie wykrycie
one pochodzić z kodu testującego. Drugi rodzaj błędu to nie wykrycie
błędu w programie. Występowanie tego rodzaju błedów testujemy poprzez
błędu w programie. Występowanie tego rodzaju błedów testujemy poprzez
wprowadzanie do programu zamierzonych błędów i sprawdzając czy nasze
wprowadzanie do programu zamierzonych błędów i sprawdzając czy nasze
Linia 248: Linia 242:
===CppUnit===
===CppUnit===


Rysunek&nbsp;[[##fig:cppunit|Uzupelnic fig:cppunit|]] przedstawia częściową hierachię klas w
[[#4.1|Rysunek 4.1]] przedstawia częściową hierachię klas w szkielecie <tt>CppUnit</tt>.
szkielecie {CppUnit}.
 
[height<nowiki>=</nowiki>,angle<nowiki>=</nowiki>90]{mod09/graphics/cppunit.eps}
 
{Częściowa hierarchia klas w szkielecie
{CppUnit}.}


Jest to bardzo mała część uwzględniająca
{{kotwica|4.1|}}[[File:cpp-9-cppunit.svg|500x350px|thumb|right|Rysunek 4.1. Częściowa hierarchia klas w szkielecie <tt>CppUnit</tt>.]]
tylko te klasy które zostaną użyte w naszym przykładzie. Proszę przede
Jest to bardzo mała część, uwzględniająca
wszystkim zwrócić uwagę na hierachię klasy {Test}. Jest to
tylko te klasy, które zostaną użyte w naszym przykładzie. Proszę przede
klasyczny wzorzec {Kompozyt} (zob.&nbsp;{gamma}), umożliwiający
wszystkim zwrócić uwagę na hierachię klasy <tt>Test</tt>. Jest to
dowolne składanie i zagnieżdżanie testów. Jest to możliwe dzieki temu
klasyczny wzorzec <tt>Kompozyt</tt> (zob. E. Gamma, R. Helm, R. Johnson, J. Vlissides <i>"Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku"</i>), umożliwiający
że klasa {TestSuite} może zawierać inne testy w tym też inne
dowolne składanie i zagnieżdżanie testów. Jest to możliwe dzieki temu,
{TestSuite}.  
że klasa <tt>TestSuite</tt> może zawierać inne testy, w tym też inne
<tt>TestSuite</tt>.


Każda klas z tej hierarchii zawiera funkcję {run(TestResult
Każda klas z tej hierarchii zawiera funkcję <tt>run(TestResult *result)</tt>, która wywołuje funkcję <tt>runTest()</tt>, którą musimy przeładować definiując nasz własny test. Klasa <tt>TestResult</tt>, a raczej jej obiekty, służą do zbierania wyników testów.  
*result)}, która wywołuje funkcję {runTest()}, którą musimy
przeładować definiując nasz własny test. Klasa {TestResult} a
raczej jej obiekty służa do zbierania wyników testów.  


Nasz przykład z {max} możemy zapisać następująco:
Nasz przykład z <tt>max</tt> możemy zapisać następująco:
#include <cppunit/TestResult.h>
#include <cppunit/TestCase.h>


class Max_test : public CppUnit::TestCase {
#include <cppunit/TestResult.h>
public:
#include <cppunit/TestCase.h><br>
class Max_test : public CppUnit::TestCase {
public:<br>
  void runTest() {
    assert(max(1,2)<nowiki>=</nowiki><nowiki>=</nowiki>1); <i>sztucznie wprowadzony błąd!</i>
    assert(max(2,1)<nowiki>=</nowiki><nowiki>=</nowiki>2);<br>
    assert(max(1,1)<nowiki>=</nowiki><nowiki>=</nowiki>1);
    assert(max(2,2)<nowiki>=</nowiki><nowiki>=</nowiki>2);<br>
    .
    .
    .
};


void runTest() {
([[media:Max_test_case.cpp | Źródło: max_test_case.cpp]])
assert(max(1,2)<nowiki>=</nowiki><nowiki>=</nowiki>1); /*sztucznie wprowadzony błąd!*/
assert(max(2,1)<nowiki>=</nowiki><nowiki>=</nowiki>2);
 
assert(max(1,1)<nowiki>=</nowiki><nowiki>=</nowiki>1);
assert(max(2,2)<nowiki>=</nowiki><nowiki>=</nowiki>2);
 
.
.
.
};
/*{mod09/code/max_test_case.cpp}{maxtestcase.cpp}*/


Powyższy test możemy wywołać następująco:
Powyższy test możemy wywołać następująco:


main() {
main() {
CppUnit::TestResult result;
CppUnit::TestResult result;
Max_test max;
Max_test max;<br>
 
max.run( &result );
max.run( &result );
}
}


Jak na razie większym kosztem uzyskaliśmy ten sam efekt co programik z
Jak na razie większym kosztem uzyskaliśmy ten sam efekt co programik z
poprzedniego podrozdziału:) Czas więc wykorzystać jakieś własności szkieletu.  
poprzedniego podrozdziału :). Czas więc wykorzystać jakieś własności szkieletu.  
 
Zaczniemy od wykorzystania zamiast standardowego makra {assert}
makr szkieletu {CppUnit}:
#include <cppunit/TestResult.h>
#include <cppunit/TestCase.h>
#include <cppunit/TestAssert.h>


class Max_test : public CppUnit::TestCase {
Zaczniemy od wykorzystania, zamiast standardowego makra <tt>assert</tt>,
public:
makr szkieletu <tt>CppUnit</tt>:  


void runTest() {
#include <cppunit/TestResult.h>
CPPUNIT_ASSERT_EQUAL(1,max(1,2)); /*sztucznie wproadzony bład!*/
#include <cppunit/TestCase.h>
CPPUNIT_ASSERT_EQUAL(2,max(2,1));
#include <cppunit/TestAssert.h><br>
class Max_test : public CppUnit::TestCase {
public:<br>
  void runTest() {
    CPPUNIT_ASSERT_EQUAL(1,max(1,2)); <i>sztucznie wproadzony bład!</i>
    CPPUNIT_ASSERT_EQUAL(2,max(2,1));<br>
    CPPUNIT_ASSERT_EQUAL(1,max(1,1));
    CPPUNIT_ASSERT_EQUAL(2,max(2,2));
    .
    .
    .
};


CPPUNIT_ASSERT_EQUAL(1,max(1,1));
([[media:Max_test_runner.cpp | Źródło: max_test_runner.cpp]])
CPPUNIT_ASSERT_EQUAL(2,max(2,2));
.
.
.
};
/*{mod09/code/max_test_runner.cpp}{maxtestrunner.cpp}*/


Jednak wykonanie tego samego kodu {main} co poprzednio, nie da
Jednak wykonanie tego samego kodu <tt>main</tt> co poprzednio nie da
żadnych wyników! To dlatego że makra {CPPUNIT_ASSERT_...} nie
żadnych wyników! To dlatego, że makra <tt>CPPUNIT_ASSERT_...</tt> nie
powoduja przerwania programu tylko zapisanie wyników do obiektu typu
powodują przerwania programu tylko zapisanie wyników do obiektu typu
{TestResult}. Najłatwiej odczytać te wyniki, nie uruchamiając
<tt>TestResult</tt>. Najłatwiej odczytać te wyniki nie uruchamiając
testów samemu tylko za pośrednictwem  obiektu {TextUi::TestRunner}.
testów samemu tylko za pośrednictwem  obiektu <tt>TextUi::TestRunner</tt>.
#include <cppunit/ui/text/TestRunner.h>


main() {
#include <cppunit/ui/text/TestRunner.h><br>
Max_test *max <nowiki>=</nowiki> new Max_test;
main() {
CppUnit::TextUi::TestRunner runner;
Max_test *max <nowiki>=</nowiki> new Max_test;
CppUnit::TextUi::TestRunner runner;<br>
runner.addTest( max );
<i>max musi byc wskaźnikiem utworzynym za pomocą new bo runner
przejmuje go na własność i sam zwalnia przydzieloną dla niego pamięć</i>
runner.run();
}


runner.addTest( max );
([[media:Max_test_runner.cpp | Źródło: max_test_runner.cpp]])
/* max musi byc wskaźnikiem utworzynym za pomocą new bo
runner przejmuje go na własność i sam zwalnia przydzieloną dla
niego pamięć
*/
runner.run();
/*{mod09/code/max_test_runner.cpp}{maxtestrunner.cpp}*/
}


Teraz uruchomienie programu spowoduje wydrukowanie na {std::cout}
Teraz uruchomienie programu spowoduje wydrukowanie na <tt>std::cout</tt>
raportu z wykonania testu. Ciągle jednak jest to niewspółmierne do
raportu z wykonania testu. Ciągle jednak jest to niewspółmierne do
włożonego wysiłku. Postaramy się więc teraz zacząć strukturyzować
włożonego wysiłku. Postaramy się więc teraz zacząć strukturyzować
nasze testy. Możemy oczywiście stworzyć kilka różnych klas  
nasze testy. Możemy oczywiście stworzyć kilka różnych klas  
testujących z inną funkcją {runTest()} każda, ale  
testujących z inną funkcją <tt>runTest()</tt> każda, ale  
zamiast tego wykorzystamy klasę {TestFixture}. Rzut oka
zamiast tego wykorzystamy klasę <tt>TestFixture</tt>. Rzut oka
na rysunek&nbsp;[[##fig:cppunit|Uzupelnic fig:cppunit|]] pokazuje że nie jest to {Test}, ale
na [[#4.1|rysunek 4.1]] pokazuje, że nie jest to <tt>Test</tt>, ale
przerobimy ją na test za pomocą {TestCaller}.
przerobimy ją na test za pomocą <tt>TestCaller</tt>.


Zaczynamy od zdefiniowanie szeregu testów w jednej klasie:
Zaczynamy od zdefiniowanie szeregu testów w jednej klasie:
#include <cppunit/TestFixture.h>
#include <cppunit/TestAssert.h>
class Max_test : public CppUnit::TestFixture {
Int i,j;
public:
Max_test():i(5),j(6) {};


void int_test() {
#include <cppunit/TestFixture.h>
CPPUNIT_ASSERT_EQUAL(1,max(1,2)); /*sztucznie wprowadzony bład!*/
#include <cppunit/TestAssert.h><br>
...
class Max_test : public CppUnit::TestFixture {
CPPUNIT_ASSERT_EQUAL(1,max(2,2)); /*sztucznie wprowadzony bład!*/
    Int i,j;
}
public:
void class_test() {
  Max_test():i(5),j(6) {};<br>
CPPUNIT_ASSERT_EQUAL( 6,max(i,j).val() );
  void int_test() {
...
    CPPUNIT_ASSERT_EQUAL(1,max(1,2)); <i>sztucznie wprowadzony bład!</i>
}
    ...
void class_ptr_test() {
    CPPUNIT_ASSERT_EQUAL(1,max(2,2)); <i>sztucznie wprowadzony bład!</i>
CPPUNIT_ASSERT_EQUAL( 6,max(&i,&j)->val() );
  }
...
  void class_test() {
}
    CPPUNIT_ASSERT_EQUAL( 6,max(i,j).val() );
void const_char_test() {
    ...
CPPUNIT_ASSERT_EQUAL(strcmp(max("abcd","acde"),"acde"),0);
  }
...
  void class_ptr_test() {
}
    CPPUNIT_ASSERT_EQUAL( 6,max(&i,&j)->val() );
void char_test() {
    ...
char s1[]<nowiki>=</nowiki>"abcde";
  }
char s2[]<nowiki>=</nowiki>"ac";
  void const_char_test() {
CPPUNIT_ASSERT_EQUAL(strcmp(max(s1,s2),s2),0);
    CPPUNIT_ASSERT_EQUAL(strcmp(max("abcd","acde"),"acde"),0);
...
    ...
CPPUNIT_ASSERT_EQUAL(strcmp(max(s2,s2),s2),1);  
  }
/*sztucznie wprowadzony bład!*/
  void char_test() {
}
    char s1[]<nowiki>=</nowiki>"abcde";
    char s2[]<nowiki>=</nowiki>"ac";
    CPPUNIT_ASSERT_EQUAL(strcmp(max(s1,s2),s2),0);
    ...
    CPPUNIT_ASSERT_EQUAL(strcmp(max(s2,s2),s2),1);  
    <i>sztucznie wprowadzony bład!</i>
}
([[media:Max_test_caller.cpp | Źródło: max_test_caller.cpp]])


Podzieliliśmy nasz test na kilka mniejszych, z których każdy testuje
Podzieliliśmy nasz test na kilka mniejszych, z których każdy testuje
zachowanie jednej klasy argumentów funkcji {max}. Zmienne używane
zachowanie jednej klasy argumentów funkcji <tt>max</tt>. Zmienne używane
wspólnie zadeklarowalismy jako składowe klasy i inicjalizujemy w
wspólnie zadeklarowalismy jako składowe klasy i inicjalizujemy w
konstruktorze. Potem pokażemy jak można wywoływać dodatkowy kod
konstruktorze. Potem pokażemy jak można wywoływać dodatkowy kod
inicjalizujacy przed wykonaniem każdej metody, ale na razie ten
inicjalizujący przed wykonaniem każdej metody, ale na razie ten
mechanizm nie jest nam potrzebny.  
mechanizm nie jest nam potrzebny.  


No ale jak wywołać te testy? Klasa {TestFixture} nie jest testem i
No, ale jak wywołać te testy? Klasa <tt>TestFixture</tt> nie jest testem i
nie posiada funkcji {runTest}, aby móc jej użyć musimy skorzystać z
nie posiada funkcji <tt>runTest</tt>. Aby móc jej użyć musimy skorzystać z
szablonu {TestCaller}. Szablon {TestCaller} przyjmuje jako swój
szablonu <tt>TestCaller</tt>. Szablon <tt>TestCaller</tt> przyjmuje jako swój
parametr klasę {TextFixture} w naszym przypadku {Max_test}. W
parametr klasę <tt>TextFixture</tt>, w naszym przypadku <tt>Max_test</tt>. W
konstruktorze obiektów tej klasy podajemy nazwę testu i adres metody
konstruktorze obiektów tej klasy podajemy nazwę testu i adres metody
klasy {Max_test} która ten test implementuje, tak skonstruowany
klasy <tt>Max_test</tt>, która ten test implementuje. Tak skonstruowany
obiekt jest już testem i możemy go przekazać do obiektu {runner}:
obiekt jest już testem i możemy go przekazać do obiektu <tt>runner</tt>:
 
main() {
 
CppUnit::TextUi::TestRunner runner;


runner.addTest( new CppUnit::TestCaller<Max_test>(
main() {<br>
"int_test",  
CppUnit::TextUi::TestRunner runner;<br>
&Max_test::int_test ) );
  runner.addTest( new CppUnit::TestCaller<Max_test>(
... /* podobnie dla reszty metod klasy Maxtest */
                        "int_test",  
                        &Max_test::int_test ) );
... <i>podobnie dla reszty metod klasy Maxtest</i><br>
runner.run();
}


runner.run();
([[media:Max_test_caller.cpp | Źródło: max_test_caller.cpp]])
/*{mod09/code/max_test_caller.html}{maxtestcaller.html}*/
}


Teraz wykonanie programu spowoduje wywołanie 5 testów. Co ważne
Teraz wykonanie programu spowoduje wywołanie 5 testów. Co ważne -
niepowodzenie ktorejś z asercji w jednym teście, przerywa wykonywanie
niepowodzenie ktorejś z asercji w jednym teście przerywa wykonywanie
tego testu, ale nie ma wpływu na wykonywanie się innych testów.  
tego testu, ale nie ma wpływu na wykonywanie się innych testów.  


Zamiast dodawać testy do "wykonywacza" pojedynczo możemy je najpierw
Zamiast dodawać testy do "wykonywacza" pojedynczo, możemy je najpierw
pogrupować za pomocą obiektów {TestSuite}:
pogrupować za pomocą obiektów <tt>TestSuite</tt>:


main() {
main() {
CppUnit::TextUi::TestRunner runner;
CppUnit::TextUi::TestRunner runner;
CppUnit::TestSuite *obj_suite <nowiki>=</nowiki> new CppUnit::TestSuite;
  CppUnit::TestSuite *obj_suite <nowiki>=</nowiki> new CppUnit::TestSuite;
CppUnit::TestSuite *ptr_suite <nowiki>=</nowiki> new CppUnit::TestSuite;
  CppUnit::TestSuite *ptr_suite <nowiki>=</nowiki> new CppUnit::TestSuite;<br>
  obj_suite->addTest( new CppUnit::TestCaller<Max_test>(
                        "int_test",
                        &Max_test::int_test ) );
  obj_suite->addTest( new CppUnit::TestCaller<Max_test>(
                        "class_test",
                        &Max_test::class_test ) );
  ptr_suite->addTest( new CppUnit::TestCaller<Max_test>(
                        "class_ptr_test",
                        &Max_test::class_ptr_test ) );
  ptr_suite->addTest( new CppUnit::TestCaller<Max_test>(
                        "const_char_test",
                        &Max_test::const_char_test ) );
  ptr_suite->addTest( new CppUnit::TestCaller<Max_test>(
                        "char_test",
                        &Max_test::char_test ) );
  runner.addTest(obj_suite);
  runner.addTest(ptr_suite);
  runner.run();
}


obj_suite->addTest( new CppUnit::TestCaller<Max_test>(
Obiekty <tt>TestSuite</tt> są  testami i możemy je dalej grupować:
"int_test",
&Max_test::int_test ) );
obj_suite->addTest( new CppUnit::TestCaller<Max_test>(
"class_test",
&Max_test::class_test ) );
ptr_suite->addTest( new CppUnit::TestCaller<Max_test>(
"class_ptr_test",
&Max_test::class_ptr_test ) );
ptr_suite->addTest( new CppUnit::TestCaller<Max_test>(
"const_char_test",
&Max_test::const_char_test ) );
ptr_suite->addTest( new CppUnit::TestCaller<Max_test>(
"char_test",
&Max_test::char_test ) );
runner.addTest(obj_suite);
runner.addTest(ptr_suite);
runner.run();
}


Obiekty {TestSuite} są testami i możemy je dalej grupować:
  CppUnit::TestSuite *max_suite <nowiki>=</nowiki> new CppUnit::TestSuite;<br>
  max_suite->addTest(obj_suite);
  max_suite->addTest(ptr_suite);
  runner.addTest(max_suite);<br>
  runner.run();


CppUnit::TestSuite *max_suite <nowiki>=</nowiki> new CppUnit::TestSuite;
Widać, że szkielet <tt>CppUnit</tt> daje nam sporo możliwości, ale ciągle
 
max_suite->addTest(obj_suite);
max_suite->addTest(ptr_suite);
runner.addTest(max_suite);
 
runner.run();
 
Widać że szkielet {CppUnit} daje nam sporo możliwości, ale ciągle
jest to niewspółmierne do włożonego wysiłku. Powodem tego jest prostota
jest to niewspółmierne do włożonego wysiłku. Powodem tego jest prostota
użytego przykładu który nie wymaga takich narzędzi. Pakiet
użytego przykładu, który nie wymaga takich narzędzi. Pakiet
{CppUnit} posiada jednak o wiele więcej możliwości między innymi:
<tt>CppUnit</tt> posiada jednak o wiele więcej możliwości, między innymi:
* W klasie {TextFixture} można przeładować funkcje {void
* W klasie <tt>TextFixture</tt> można przeładować funkcje <tt>void setUp()</tt> i <tt>void tearDown()</tt>, które będą wywoływane odpowiednio przed i po wykonaniu każdego testu. Mogą być użyte do konstrukcji środowiska testowego i jego rozmontowania. Nie używałem tej możliwości, bo nie była ona potrzebna w tak prostym przykładzie.
setUp()} i {void tearDown()} które będą wywoływane odpowiednio
* Bardzo często chcemy wykonać razem wszystkie testy zdefiniowane w jednej klasie. <tt>CppUnit</tt> dostarcza makr ułatwiających grupowanie wszystkich metod danej klasy w jeden <tt>TestSuite</tt>.
przed i po wykonaniem każdego testu. Mogą być użyte do konstrukcji
* Szkielet <tt>CppUnit</tt> oferuje poza <tt>CPPUNIT_ASSERT_EQUAL</tt> szereg innych makr ulatwiających pisanie testów.
środowiska testowego i jego rozmontowanie. Nie używałem tej możliwości bo nie była ona potrzebna w tak prostym przykładzie.
* Stosunkowo łatwo można zmienić format wyświetlania wyników testów np. dodanie  
* Bardzo często chcemy wykonać razem wszystkie testy zdefiniowane
w jednej klasie. {CppUnit} dostarcza makr ułatwiających
grupowanie wszystkich metod danej klasy w jeden {TestSuite}.
* Szkielet {CppUnit} oferuje poza {CPPUNIT_ASSERT_EQUAL}
szereg innych makr, ulatwiających pisanie testów.
* Stosunkowo łatwo można zmienić format wyświetlania wyników testów
np. dodanie  
 
runner.setOutputter( new CppUnit::XmlOutputter(
&runner.result(),
std::cout ) );


spowoduje wypisanie wyników testu w formacie XML.  
      runner.setOutputter( new CppUnit::XmlOutputter(
                              &runner.result(),
                              std::cout ) );


==Podsumowanie==
spowoduje wypisanie wyników testu w formacie XML.

Aktualna wersja na dzień 11:10, 3 paź 2021

Wstęp

Programowanie rozumiane jako pisanie kodu jest tylko częścią procesu tworzenia oprogramowania. Analiza i opis tego procesu jest przedmiotem inżynierii oprogramowania i znacznie wykracza poza ramy tego wykładu. Niemniej chciałbym pokrótce w tym wykładzie poruszyć jedno zagadnienie związane bezpośrednio z programowaniem - testowanie. Testowanie jest nieodłączną częścią programowania i powinno być obowiązkiem każdego programisty. Jak będę się starał Państwa przekonać, testowanie to może być coś więcej niż "tylko" sprawdzenie poprawności kodu.

Testowanie

Testowanie wydaje się oczywistą koniecznością w przypadku każdego programu komputerowego (choć co rok zdarza mi się spotkać studentów przekonanych o swojej nieomylności :)). Mniej oczywiste jest stwierdzenie kto, gdzie, kiedy i co ma testować. To w ogólności bardzo złożony problem, ale tu chciałbym się ograniczyć do tzw. testów jednostkowych. Wyrażenie "test jednostkowy" należy interpretować jako test jednej jednostki. Przez pojedynczą jednostkę będziemy rozumieli: funkcję, metodę lub klasę. Zadaniem takiego testu jest sprawdzenie czy dana jednostka działa poprawnie.

Dlaczego w ogóle pisać takie testy? Czy nie wystarczy przetestowanie całego programu? Testować cały program też oczywiście trzeba. Służą do tego liczne testy odbioru, integracyjne itp., wykonywane poprzez dedykowane zespoły. Ale im większą część kodu testujemy, tym trudniejsze są testy i tym trudniej będzie znaleźć przyczynę wykrytej nieprawidłowości działania programu. Testy jednostkowe wykrywają błędy (a przynajmniej ich część) "u źródła", często w bardzo prostym kodzie, a więc ich poprawianie może być dużo szybsze.

Jak się zastanowić, to testowanie każdej wykonanej jednostki przed użyciem jej w dalszym kodzie powinno być oczywistą koniecznością. No, ale równie oczywiste jest, że należy uprawiać sporty, nie palić papierosów, nie jeździć po pijanemu, itp. Statystyki dobitnie jednak pokazują, że natura człowiecza grzeszną jest i łatwo ulegamy słabościom, w tym wypadku pokusie nietestowania programów, a powodem jest jeden z siedmiu grzechów głównych czyli lenistwo. Testy nie piszą, ani nie wykonuja się same, w rzeczywistości wymagają całkiem sporego nakładu pracy (czasami większego niż pisanie testowanego kodu!). Część programistów traktuje ten wysiłek jako "czas stracony", tzn. nie wykorzystany na pisanie kodu, za który im płacą.

To wszystko jest prawdą, ale w rzeczywistości nie możemy uniknąć tej straty czasu. Jest to tylko kwestia wyboru gdzie i ile tego czasu stracimy: czy na pisanie testów w trakcie kodowania jednostek i poprawianie prostych błędów, czy na szukanie błędów w dużo większych fragmentach programu? Doświadczenie wykazuje, że czas potrzebny na znalezienie i poprawienie błędu jest w tym drugim przypadku dużo większy, w krańcowych przypadkach poprawienie programu może być wręcz niemożliwe. Testy jednostkowe skalują się liniowo z rozmiarem pisanego kodu, a nie ekspotencjalnie jak np. testy integracyjne. Oczywiście testy jednostkowe nie rozwiążą wszystkich problemów, jeśli mamy zły projekt całości to nic nam nie pomogą idealnie działające jednostki, ale przynajmniej będzie nam łatwiej się o tym przekonać.

Jak już napisałem testy służą do sprawdzania poprawności działania danej jednostki: ciągłego sprawdzanie działania tej jednostki. Ponieważ kod się nieustannie zmienia, powinniśmy wykonywać testy cały czas aby sprawdzić czy te zmiany wynikające, np. z poprawienia wykrytych błędów, nie wprowadziły nowych usterek. Aby to było możliwe testy musza być łatwe do wykonywania czyli zautomatyzowane. Nie mogą polegać na tym, że puszczamy program, a następnie przeglądamy wydruk. Musimy "nacisnąć guzik" a w odpowiedzi zapali nam się szereg "światełek": zielone wskażą testy zaliczone, a czerwone testy niezaliczone. Taki automatyczny proces testujący może być zintegrowany z innymi narzędzimi programistycznymi, takimi jak np. make.

Refaktoryzacja

Wysiłek włożony w napisanie takich powtarzalnych testów nie jest zaniedbywalny, ale korzyści są duże. Możliwość przetestowania kodu w dowolnym momencie to więcej niż tylko świadomość, że nasz obecny kod przeszedł testy. Taka możliwość, w połączeniu z narzędziami zarządzającymi kodem źródłowym, pozwala na bezpieczne dokonywanie zmian według schematu: zmieniamy, testujemy, jeśli testy się nie powiodą szukamy błedów, jeśli ich nie znajdujemy to cofamy zmiany. Takie podejście jest szczególnie pomocne, a właściwie niezbędne, podczas programowania przyrostowego i refaktoryzacji.

Programowanie przyrostowe to technika zalecana dla większości projektów, która polega na programowaniu "malymi krokami", czyli na kolejnym dodawaniu nowych funkcji do istniejącego działajacego kodu. Testy umożliwiają sprawdzenie czy dodany kod nie wprowadził błędów do starej części. Oczywiście każdy przyrost wymaga stworzenia nowego zestawu testów.

Refaktoryzacja to zmiana kodu bez zmiany jego funkcjonalności w celu poprawy jego jakości.

Projektowanie sterowane testami

Pisanie testu sprawdza również nasze zrozumienie tego, co dana jednostka ma robić. W tym sensie testy stanowią sformalizowany zapis wymagań. To zaleta, którą trudno przecenić. Jeżeli nie wiemy jak przestestować daną jednostkę, to najprawdopodobniej nie powinniśmy się wcale brać za jej kodowanie. Dlatego niektóre metodologie (np. extreme programming) zalecają projektowanie sterowane testami, czyli zaczęcie pisania programu od pisania testów do niego. Przebieg pracy przy takim podejściu wygląda następująco:

  1. Piszemy test
  2. Kompilujemy test
  3. Test się nie kompiluje
  4. Piszemy tyle kodu aby test sie skompilował
  5. Test się kompiluje
  6. Wykonujemy test
  7. Test najprawdpodobniej nie wykonuje się poprawnie
  8. Poprawiamy/dopisujemy kod tak aby test się wykonał
  9. Test wykonuje się poprawnie

Osobiście wydaje mi się, że zalety tego podejścia są ogromne. Nawet jeśli brakuje czasu i ochoty na pisanie testów, to należy po prostu zastanowić się nad sposobem przetestowania naszego kodu zanim zaczniemy go pisać. To znakomicie zmusza do ścisłego określenia tego, co właściwie nasz program ma robić.

Testy

Po tej całej propagandzie - czas na testowanie w praktyce. Przetestujemy przykłady wprowadzone w poprzednich rozdziałach, zaczynając od funkcji max:

template<typename T> T max(T a,T b) {return (a>b)?a:b;}

Być może część z Państwa oburzy się: jak to? testować tę jedną linijkę? Gdyby chodziło o tę jedną linijkę to rzeczywiście wystarczy na nią popatrzeć (i mieć wiarę w kompilator), ale przypominam, że dodaliśmy do niej dwie przeciążone wersje i dwie specjalizacje. A to oznacza, że ta linijka nie działa poprawnie w każdym przypadku. Napiszemy więc testy, które to wyłapią. Jak już pisałem będzie to jednoczesne zdefiniowanie tego jak funkcja max ma działać. Po chwili zastanowienia ustalamy więc, że nasza funkcja max zwraca:

  • większą z dwu przekazanych wartości (większą w sensie porównania operatorem >)
  • jeśli argumentami są wskaźniki to max zwraca wskaźnik na większą wartość
  • jeśli argumentami są wskaźniki na char to max traktuje je jako napisy i zwraca wskaźnik do napisu większego zgodnie z uporządkowaniem leksykalnym.

Testować funkcję max będziemy poprzez porównanie wartości zwracanej do wartości poprawnej, którą sami wskażemy:

assert(max(1,2)==2);
assert(max(2,1)==2);

Należy też sprawdzić przypadek symetryczny

assert(max(1,1)==1);
assert(max(2,2)==2);

Testowanie szablonu jest trudne, bo w zasadzie musimy rozważyć przekazanie argumentów dowolnego typu. Co więcej, ten typ wcale nie musi posiadać operatora porównania ==. Aby sprawdzić działanie max na jakimś typie niewbudowanym posłużymy się własną klasą:

class Int {
private:
 int _val;
public:
 Int(int i):_val(i) {};
 int val()const {return _val;};
 friend bool operator>(const Int &a,const Int &b)  {
 return a._val>b._val; 
 }
};

Kod testowy może teraz wyglądać następująco:

Int i(5);
Int j(6);
assert(max(i,j).val() == 6);
assert(max(j,i).val() == 6);
assert(max(j,j).val() == 6);
assert(max(i,i).val() == 5);

Następnie testujemy wersję wskaźnikową:

assert(max(&i,&j)->val() == 6);
assert(max(&j,&i)->val() == 6);
assert(max(&j,&j)->val() == 6);
assert(max(&i,&i)->val() == 5);

i na koniec wersję dla napisów (const char *):

assert(0==strcmp(max("abcd","acde"),"acde"));
assert(0==strcmp(max("acde","abcd"),"acde"));
assert(0==strcmp(max("abcd","abcd"),"abcd"));
assert(0==strcmp(max("acde","acde"),"acde"));

oraz (char *):

char s1[]="abcde";
char s2[]="ac";
assert(0==strcmp(max(s1,s2),s2));
assert(0==strcmp(max(s2,s1),s2));
assert(0==strcmp(max(s1,s1),s1));
assert(0==strcmp(max(s2,s2),s2));

Napisy zostały tak dobrane, żeby miały pierwszy znak identyczny, ponieważ procedura ogólna dla wskaźników porównuje właśnie pierwsze znaki. To pokazuje jak ważny jest wybór danych testowych. Testy jednostkowe są testami "białej skrzynki", tzn. piszący testy ma wgląd do implementacji testowanego kodu (powinien to być ten sam programista), można więc przygotować dane testowe tak, aby pokrywały możliwie wiele ścieżek wykonywania programu, warunków brzegowych itp.

Oczywiście bezbłędne przejście powyższych testów nie gwarantuje nam jeszcze poprawności funkcji max (tego zresztą nie zagwarantuje nam żaden test), ale bardzo zwiększa prawdopodobieństwo tego, że tak jest.

Pozostaje nam do wytestowanie procedura szukająca maksimum w tablicy. To zadanie zostawiam jako ćwiczenie dla czytelników.


Powyższy przykład może budzić pewne obawy. Kod funkcji max liczy około 20 linii, a kod testujący 40. No cóż, testowanie kosztuje, tylko czy możemy sobie pozwolić na zrezygnowanie z niego? Rozważmy powyższy przykład, te dwadzieścia linii kodu definiującego szablon max jest dużo bardziej skomplikowane niż te czterdzieści linii kodu testujacego, tak że jeśli porównamy czas pisania, to nie wypada to już tak źle. Kod szablonu max zawiera przeciążenia i specjalizację, czyli wykorzystany jest algorytm rozstrzygania przeciążenia. Osobiście nie ufam swojej znajomości tego algorytmu na tyle, aby nie sprawdzić kodu. Jeżeli i tak wykonujemy jakieś testy to warto zainwestować trochę więcej pracy i przygotować porządny zestaw testujący. Może też się zdarzyć, że będziemy chcieli dodać kolejne przeciążenia, które mogą wpłynąć w niezamierzony sposób na przeciążenia już istniejące. Gotowy programik testujący pozwoli nam to szybko wykryć.

Pozostaje jeszcze sprawa poprawności samego programu testującego, w końcu to też jest kod i może zawierać błędy. Czy więc musimy pisać kod testujący program testujący, i tak dalej w nieskończoność? Na szczęście nie, błędy w kodzie testującym mogą mieć dwa efekty. Pierwszy to wykazanie błędu, który nie jest błędem, w tej sytuacji taki błąd wykrywa się sam. Musimy tylko pamiętać podczas szukania źródeł wykazanych przez program testujacy usterek, że mogą one pochodzić z kodu testującego. Drugi rodzaj błędu to nie wykrycie błędu w programie. Występowanie tego rodzaju błedów testujemy poprzez wprowadzanie do programu zamierzonych błędów i sprawdzając czy nasze testy je wykryją.

CppUnit

Rysunek 4.1 przedstawia częściową hierachię klas w szkielecie CppUnit.

Rysunek 4.1. Częściowa hierarchia klas w szkielecie CppUnit.

Jest to bardzo mała część, uwzględniająca tylko te klasy, które zostaną użyte w naszym przykładzie. Proszę przede wszystkim zwrócić uwagę na hierachię klasy Test. Jest to klasyczny wzorzec Kompozyt (zob. E. Gamma, R. Helm, R. Johnson, J. Vlissides "Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku"), umożliwiający dowolne składanie i zagnieżdżanie testów. Jest to możliwe dzieki temu, że klasa TestSuite może zawierać inne testy, w tym też inne TestSuite.

Każda klas z tej hierarchii zawiera funkcję run(TestResult *result), która wywołuje funkcję runTest(), którą musimy przeładować definiując nasz własny test. Klasa TestResult, a raczej jej obiekty, służą do zbierania wyników testów.

Nasz przykład z max możemy zapisać następująco:

#include <cppunit/TestResult.h>
#include <cppunit/TestCase.h>
class Max_test : public CppUnit::TestCase { public:
void runTest() { assert(max(1,2)==1); sztucznie wprowadzony błąd! assert(max(2,1)==2);
assert(max(1,1)==1); assert(max(2,2)==2);
. . . };

( Źródło: max_test_case.cpp)

Powyższy test możemy wywołać następująco:

main() {
CppUnit::TestResult result;
Max_test max;
max.run( &result ); }

Jak na razie większym kosztem uzyskaliśmy ten sam efekt co programik z poprzedniego podrozdziału :). Czas więc wykorzystać jakieś własności szkieletu.

Zaczniemy od wykorzystania, zamiast standardowego makra assert, makr szkieletu CppUnit:

#include <cppunit/TestResult.h>
#include <cppunit/TestCase.h>
#include <cppunit/TestAssert.h>
class Max_test : public CppUnit::TestCase { public:
void runTest() { CPPUNIT_ASSERT_EQUAL(1,max(1,2)); sztucznie wproadzony bład! CPPUNIT_ASSERT_EQUAL(2,max(2,1));
CPPUNIT_ASSERT_EQUAL(1,max(1,1)); CPPUNIT_ASSERT_EQUAL(2,max(2,2)); . . . };

( Źródło: max_test_runner.cpp)

Jednak wykonanie tego samego kodu main co poprzednio nie da żadnych wyników! To dlatego, że makra CPPUNIT_ASSERT_... nie powodują przerwania programu tylko zapisanie wyników do obiektu typu TestResult. Najłatwiej odczytać te wyniki nie uruchamiając testów samemu tylko za pośrednictwem obiektu TextUi::TestRunner.

#include <cppunit/ui/text/TestRunner.h>
main() { Max_test *max = new Max_test; CppUnit::TextUi::TestRunner runner;
runner.addTest( max ); max musi byc wskaźnikiem utworzynym za pomocą new bo runner przejmuje go na własność i sam zwalnia przydzieloną dla niego pamięć runner.run(); }

( Źródło: max_test_runner.cpp)

Teraz uruchomienie programu spowoduje wydrukowanie na std::cout raportu z wykonania testu. Ciągle jednak jest to niewspółmierne do włożonego wysiłku. Postaramy się więc teraz zacząć strukturyzować nasze testy. Możemy oczywiście stworzyć kilka różnych klas testujących z inną funkcją runTest() każda, ale zamiast tego wykorzystamy klasę TestFixture. Rzut oka na rysunek 4.1 pokazuje, że nie jest to Test, ale przerobimy ją na test za pomocą TestCaller.

Zaczynamy od zdefiniowanie szeregu testów w jednej klasie:

#include <cppunit/TestFixture.h>
#include <cppunit/TestAssert.h>
class Max_test : public CppUnit::TestFixture { Int i,j; public: Max_test():i(5),j(6) {};
void int_test() { CPPUNIT_ASSERT_EQUAL(1,max(1,2)); sztucznie wprowadzony bład! ... CPPUNIT_ASSERT_EQUAL(1,max(2,2)); sztucznie wprowadzony bład! } void class_test() { CPPUNIT_ASSERT_EQUAL( 6,max(i,j).val() ); ... } void class_ptr_test() { CPPUNIT_ASSERT_EQUAL( 6,max(&i,&j)->val() ); ... } void const_char_test() { CPPUNIT_ASSERT_EQUAL(strcmp(max("abcd","acde"),"acde"),0); ... } void char_test() { char s1[]="abcde"; char s2[]="ac"; CPPUNIT_ASSERT_EQUAL(strcmp(max(s1,s2),s2),0); ... CPPUNIT_ASSERT_EQUAL(strcmp(max(s2,s2),s2),1); sztucznie wprowadzony bład! }

( Źródło: max_test_caller.cpp)

Podzieliliśmy nasz test na kilka mniejszych, z których każdy testuje zachowanie jednej klasy argumentów funkcji max. Zmienne używane wspólnie zadeklarowalismy jako składowe klasy i inicjalizujemy w konstruktorze. Potem pokażemy jak można wywoływać dodatkowy kod inicjalizujący przed wykonaniem każdej metody, ale na razie ten mechanizm nie jest nam potrzebny.

No, ale jak wywołać te testy? Klasa TestFixture nie jest testem i nie posiada funkcji runTest. Aby móc jej użyć musimy skorzystać z szablonu TestCaller. Szablon TestCaller przyjmuje jako swój parametr klasę TextFixture, w naszym przypadku Max_test. W konstruktorze obiektów tej klasy podajemy nazwę testu i adres metody klasy Max_test, która ten test implementuje. Tak skonstruowany obiekt jest już testem i możemy go przekazać do obiektu runner:

main() {
CppUnit::TextUi::TestRunner runner;
runner.addTest( new CppUnit::TestCaller<Max_test>( "int_test", &Max_test::int_test ) ); ... podobnie dla reszty metod klasy Maxtest
runner.run(); }

( Źródło: max_test_caller.cpp)

Teraz wykonanie programu spowoduje wywołanie 5 testów. Co ważne - niepowodzenie ktorejś z asercji w jednym teście przerywa wykonywanie tego testu, ale nie ma wpływu na wykonywanie się innych testów.

Zamiast dodawać testy do "wykonywacza" pojedynczo, możemy je najpierw pogrupować za pomocą obiektów TestSuite:

main() {
CppUnit::TextUi::TestRunner runner;
 CppUnit::TestSuite *obj_suite = new CppUnit::TestSuite;
 CppUnit::TestSuite *ptr_suite = new CppUnit::TestSuite;
obj_suite->addTest( new CppUnit::TestCaller<Max_test>( "int_test", &Max_test::int_test ) ); obj_suite->addTest( new CppUnit::TestCaller<Max_test>( "class_test", &Max_test::class_test ) ); ptr_suite->addTest( new CppUnit::TestCaller<Max_test>( "class_ptr_test", &Max_test::class_ptr_test ) ); ptr_suite->addTest( new CppUnit::TestCaller<Max_test>( "const_char_test", &Max_test::const_char_test ) ); ptr_suite->addTest( new CppUnit::TestCaller<Max_test>( "char_test", &Max_test::char_test ) ); runner.addTest(obj_suite); runner.addTest(ptr_suite); runner.run(); }

Obiekty TestSuite są testami i możemy je dalej grupować:

 CppUnit::TestSuite *max_suite = new CppUnit::TestSuite;
max_suite->addTest(obj_suite); max_suite->addTest(ptr_suite); runner.addTest(max_suite);
runner.run();

Widać, że szkielet CppUnit daje nam sporo możliwości, ale ciągle jest to niewspółmierne do włożonego wysiłku. Powodem tego jest prostota użytego przykładu, który nie wymaga takich narzędzi. Pakiet CppUnit posiada jednak o wiele więcej możliwości, między innymi:

  • W klasie TextFixture można przeładować funkcje void setUp() i void tearDown(), które będą wywoływane odpowiednio przed i po wykonaniu każdego testu. Mogą być użyte do konstrukcji środowiska testowego i jego rozmontowania. Nie używałem tej możliwości, bo nie była ona potrzebna w tak prostym przykładzie.
  • Bardzo często chcemy wykonać razem wszystkie testy zdefiniowane w jednej klasie. CppUnit dostarcza makr ułatwiających grupowanie wszystkich metod danej klasy w jeden TestSuite.
  • Szkielet CppUnit oferuje poza CPPUNIT_ASSERT_EQUAL szereg innych makr ulatwiających pisanie testów.
  • Stosunkowo łatwo można zmienić format wyświetlania wyników testów np. dodanie
      runner.setOutputter( new CppUnit::XmlOutputter( 
                              &runner.result(),
                              std::cout ) );

spowoduje wypisanie wyników testu w formacie XML.