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

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Mirek (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 12 wersji utworzonych przez 3 użytkowników)
Linia 119: Linia 119:
===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
<tt>max</tt>:  
<tt>max</tt>:  
Linia 125: Linia 125:
  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 <tt>max</tt>
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
<tt>max</tt> zwraca:  
<tt>max</tt> zwraca:  
* większą z dwu przekazanych wartości (większą w sensie porównania operatorem <tt>></tt>).
* większą z dwu przekazanych wartości (większą w sensie porównania operatorem <tt>></tt>)
* jeśli argumentami są wskaźniki to <tt>max</tt> zwraca wskaźnik na większą wartość.
* jeśli argumentami są wskaźniki to <tt>max</tt> zwraca wskaźnik na większą wartość
* 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.
* 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.


Testować funkcję <tt>max</tt> 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);
Linia 149: Linia 149:
  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 <tt><nowiki>=</nowiki><nowiki>=</nowiki></tt>. Aby sprawdzić działanie <tt>max</tt> na jakimś typie niewbudowanym posłużymy się własną klasą:
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ą:


Linia 196: Linia 196:
  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 <tt>max</tt> (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.


Linia 214: Linia 214:


Powyższy przykład może budzić pewne obawy. Kod funkcji <tt>max</tt> 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 <tt>max</tt> 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
<tt>max</tt> zawiera przeciążenia i specjalizację, czyli wykorzystany jest
<tt>max</tt> zawiera przeciążenia i specjalizację, czyli wykorzystany jest
Linia 223: Linia 223:
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 230: 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 242: Linia 242:
===CppUnit===
===CppUnit===


Rysunek 4.1<font color=red>&nbsp;{[##fig:cppunit|Uzupelnic fig:cppunit|]}</font> przedstawia częściową hierachię klas w szkielecie <tt>CppUnit</tt>.
[[#4.1|Rysunek 4.1]] przedstawia częściową hierachię klas w szkielecie <tt>CppUnit</tt>.


<center>
{{kotwica|4.1|}}[[File:cpp-9-cppunit.svg|500x350px|thumb|right|Rysunek 4.1. Częściowa hierarchia klas w szkielecie <tt>CppUnit</tt>.]]
<div class="thumb"><div style="width:500px;">
Jest to bardzo mała część, uwzględniająca
<flash>file=cpp-9-cppunit.swf|width=500|height=350</flash>
tylko te klasy, które zostaną użyte w naszym przykładzie. Proszę przede
<div.thumbcaption>Rysunek 4.1. Częściowa hierarchia klas w szkielecie <tt>CppUnit</tt>.</div>
</div></div>
</center>
 
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 <tt>Test</tt>. Jest to
wszystkim zwrócić uwagę na hierachię klasy <tt>Test</tt>. Jest to
klasyczny wzorzec <tt>Kompozyt</tt> (zob. <i>"patterns"</i>), umożliwiający
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
dowolne składanie i zagnieżdżanie testów. Jest to możliwe dzieki temu
dowolne składanie i zagnieżdżanie testów. Jest to możliwe dzieki temu,
że klasa <tt>TestSuite</tt> może zawierać inne testy w tym też inne
że klasa <tt>TestSuite</tt> może zawierać inne testy, w tym też inne
<tt>TestSuite</tt>.
<tt>TestSuite</tt>.


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ża do zbierania wyników testów.  
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.  


Nasz przykład z <tt>max</tt> możemy zapisać następująco:
Nasz przykład z <tt>max</tt> możemy zapisać następująco:
Linia 277: Linia 271:
  };
  };


([http://osilek.mimuw.edu.pl/images/e/ee/Max_test_case.cpp Źródło: Max_test_case.cpp])
([[media:Max_test_case.cpp | Źródło: max_test_case.cpp]])


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


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 <tt>assert</tt>
Zaczniemy od wykorzystania, zamiast standardowego makra <tt>assert</tt>,
makr szkieletu <tt>CppUnit</tt>:  
makr szkieletu <tt>CppUnit</tt>:  


Linia 308: Linia 302:
  };
  };


([http://osilek.mimuw.edu.pl/images/6/68/Max_test_runner.cpp Źródło: Max_test_runner.cpp])
([[media:Max_test_runner.cpp | Źródło: max_test_runner.cpp]])


Jednak wykonanie tego samego kodu <tt>main</tt> co poprzednio, nie da
Jednak wykonanie tego samego kodu <tt>main</tt> co poprzednio nie da
żadnych wyników! To dlatego że makra <tt>CPPUNIT_ASSERT_...</tt> 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
<tt>TestResult</tt>. 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 <tt>TextUi::TestRunner</tt>.
testów samemu tylko za pośrednictwem  obiektu <tt>TextUi::TestRunner</tt>.


Linia 326: Linia 320:
  }
  }


([http://osilek.mimuw.edu.pl/images/6/68/Max_test_runner.cpp Źródło: Max_test_runner.cpp])
([[media:Max_test_runner.cpp | Źródło: max_test_runner.cpp]])


Teraz uruchomienie programu spowoduje wydrukowanie na <tt>std::cout</tt>
Teraz uruchomienie programu spowoduje wydrukowanie na <tt>std::cout</tt>
Linia 334: Linia 328:
testujących z inną funkcją <tt>runTest()</tt> każda, ale  
testujących z inną funkcją <tt>runTest()</tt> każda, ale  
zamiast tego wykorzystamy klasę <tt>TestFixture</tt>. Rzut oka
zamiast tego wykorzystamy klasę <tt>TestFixture</tt>. Rzut oka
na rysunek 4.1 <font color=red>&nbsp;{[##fig:cppunit|Uzupelnic fig:cppunit|]}</font> pokazuje że nie jest to <tt>Test</tt>, ale
na [[#4.1|rysunek 4.1]] pokazuje, że nie jest to <tt>Test</tt>, ale
przerobimy ją na test za pomocą <tt>TestCaller</tt>.
przerobimy ją na test za pomocą <tt>TestCaller</tt>.


Linia 370: Linia 364:
     <i>sztucznie wprowadzony bład!</i>
     <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
Linia 375: Linia 370:
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 <tt>TestFixture</tt> nie jest testem i
No, ale jak wywołać te testy? Klasa <tt>TestFixture</tt> nie jest testem i
nie posiada funkcji <tt>runTest</tt>, 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 <tt>TestCaller</tt>. Szablon <tt>TestCaller</tt> przyjmuje jako swój
szablonu <tt>TestCaller</tt>. Szablon <tt>TestCaller</tt> przyjmuje jako swój
parametr klasę <tt>TextFixture</tt> w naszym przypadku <tt>Max_test</tt>. 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 <tt>Max_test</tt> 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 <tt>runner</tt>:
obiekt jest już testem i możemy go przekazać do obiektu <tt>runner</tt>:


Linia 395: Linia 390:
  }
  }


([http://osilek.mimuw.edu.pl/images/e/e2/Max_test_caller.cpp Źródło: Max_test_caller.cpp])
([[media:Max_test_caller.cpp | Źródło: max_test_caller.cpp]])


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 <tt>TestSuite</tt>:
pogrupować za pomocą obiektów <tt>TestSuite</tt>:


Linia 436: Linia 431:
  runner.run();
  runner.run();


Widać że szkielet <tt>CppUnit</tt> daje nam sporo możliwości, ale ciągle
Widać, że szkielet <tt>CppUnit</tt> 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
<tt>CppUnit</tt> 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 <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 wykonaniem każdego testu. Mogą być użyte do konstrukcji środowiska testowego i jego rozmontowanie. Nie używałem tej możliwości bo nie była ona potrzebna w tak prostym przykładzie.
* 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.
* 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>.
* 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>.
* Szkielet <tt>CppUnit</tt> oferuje poza <tt>CPPUNIT_ASSERT_EQUAL</tt> szereg innych makr, ulatwiających pisanie testów.
* Szkielet <tt>CppUnit</tt> oferuje poza <tt>CPPUNIT_ASSERT_EQUAL</tt> szereg innych makr ulatwiających pisanie testów.
* Stosunkowo łatwo można zmienić format wyświetlania wyników testów np. dodanie  
* Stosunkowo łatwo można zmienić format wyświetlania wyników testów np. dodanie  


Linia 450: Linia 445:


spowoduje wypisanie wyników testu w formacie XML.
spowoduje wypisanie wyników testu w formacie XML.
==Zarządzanie kodem źródłowym==

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.