background image

Automatyczne testowanie oprogramowania

Łukasz Juszkiewcz

background image

Automatyczne   testowanie   jest   jednym   z   głównych   postulatów  eXtreme 

Programming  (XP).   Automatyzacja   polega   na   użyciu   oprogramowania   do   sterowania 
wykonaniem testów, porównywania ich wyników z oczekiwanymi, czy raportowania.

Testowanie   oprogramowania   wymaga   dostarczenia   przypadku   testowego   (test 

case) – częściej całego zestawu przypadków testowych dla testowanego systemu (test 

case   suite)   –   przez   programistę,   uruchomienia   programu   z   użyciem   przygotowanego 
zestawu   przypadków   testowych   i   sprawdzeniu,   czy   przy   danych   wejściowych 

pochodzących z przpadku testowego zachowanie programu jest zgodne z oczekiwanym. 
Wszystkie te trzy aspekty testowania oprogramowania mogą zostać zautomatyzowane w 

mniejszym lub większym stopniu.

Z   powyższych   aspektów,   najprostszym   do   zautomatyzowania   jest   wykonanie 

przypadków testowych. Dla aplikacji, które nie wchodzą w interakcję z użytkownikami 
(lub ich interakcja ogranicza się do komend wpisywanych klawiaturą), łatwo jest napisać 

mały program, nazywany skryptem testowym (test script), generujący odpowiednie dane 
wejściowe. Nieco trudniej wykonać to dla programów z graficznym interfejsem, jednak 

istnieją   narzędzia   pozwalające   testować   tego   typu   aplikacje.   Potrafią   one   nagrywać 
sekwencje wykonywanych operacji w testowanym programie, jednak podstawową wadą 

tych   narzędzi   jest   zależność   wyniku   testu   od   umiejscowienia   poszczególnych 
komponentów   interfejsu   na   ekranie.   W   razie   zmiany   ich   ułożenia   lub   usunięcia 

istniejącego   komponentu   testy   przestają   działać   poprawnie   i   należy   je   ponownie 
zdefiniować.

Czasami   także   sprawdzenie,   czy   dane   wyjściowe   aplikacji   są   zgodne   z 

oczekiwanymi jest łatwe do zautomatyzowania – na przykład w programie wykonującym 

jakieś matematyczne obliczenia na danych wejściowych i zapisującym ich rezultat w 
pliku   wyjściowym.   Istnieje   jednak   wiele   sytuacji,   kiedy   sprawdzenie   zachowania 

aplikacji   nie   jest   możliwe   poprzez   zwykłe   porównanie   z   oczekiwanymi   danymi   – 
przykładowo,   w   programie   do   kompresji   audio   test   prawidłowego   działania   aplikacji 

polega na wysłuchaniu skompresowanej próbki i subiektywnej ocenie jej jakości.

Jednak w rozwoju sporej części oprogramowania automatyczne testowanie jest 

powszechną praktyką, a narzędzia je wspomagające są szeroko dostępne, wiele z nich to 
wolne czy otwarte oprogramowanie.

Automatycznie generowane przypadki testowe są jednak dużo bardziej złożonym 

problemem   i   użycie   takich   automatycznie   tworzonych   przypadków   testowych   jest   o 

wiele   rzadsze.     Jednym   ze   sposobów   generowania   przypadków   testowych   jest 
testowanie   na   poziomie   modelu   (model-based   testing),   gdzie   model   systemu   jest 

używany do automatycznego tworzenia przypadków testowych, jednak nie zakończono 
badań nad sposobami realizacji tego zadania.

W   książce  Wzorce   projektowe.   Elementy   oprogramowania   obiektowego 

wielokrotnego   użytku  John   Vlissides,   Ralph   Johnson,   Erich   Gamma,   Richard   Helm 
(“Banda Czworga”  -  “Gang of Four”)  przedstawili wzorzec projektowy Model-Widok-

Kontroler (Model-View-Controler – MVC). Pokazuje on, jak separować warstwy aplikacji 
odpowiedzialne za logikę (Model) od warstwy opisującej interfejs użytkownika (Widok, 

Kontroler).   Jak   wiadomo,   automatyczne   testowanie   aplikacji   na   poziomie   Widoku 
sprawia   problemy,   automatycznie   testuje   się   głównie   model   aplikacji.   Standardowa 

metoda   to   stworzenie   obiektu   testowanej   klasy   (klas),   wykonaine   kilku   metod   i 
sprawdzenie   wyników   ich   działania.   Dodatkowo   powinny   być   spełnione   następujące 

warunki:

1. możliwośc uruchomienia poszczególnych testów z całego zestawu;

background image

2. niezależne wykonywanie testów:  wynik  (np.  niepowodzenie) jednego testu nie 

powinno mieć wpływu na pozostałe – umożliwi to uzyskanie jak najawiększej ilości 
informacji na temat działania całego systemu;

3. tworzenie raportów z wykonanych testów ze szczegółowymi danymi i statystykami 

–   np.   w   przypadku   niepowodzenia   powinny   zawierać   wartości   oczekiwane   i 

otrzymane.

Testowanie w eXtreme Programming.

XP testy dzieli się na trzy rodzaje:

1. jednostkowe – służą do testowania pojedynczej klasy (modułu) odizolowanej od 

pozostałych komponentów; pisane przez programistów;

2. akceptacyjne   –   opisują   oczekiwania   klienta   wobec   systemu;   tworzone   przez 

klienta – z reguły w prostym języku skryptowym, rzadziej w języku naturalnym, 

kodowane następnie przez programistę;

3. interaktywne – nieautomatyczne testy służące do wykrywania błędów wizualnych: 

nieprawidłowego   rozmieszczenia   elementów   interfejsu   użytkownika,   mało 
estetycznego   wyglądu   komponentów;   jeśli   testy   jednostkowe   zostały   dobrze 

zaprojektowane, w testach interaktywnych nie powinny istnieć błędy modelu, jak 
nieprawidłowe   wyniki   obliczeń,   nieprawidłowa   implementacja   komunikacji   z 

pozostałymi komponentami systemu.

Standardowo,   dodawanie   nowej   funkcjonalności   do   systemu   jest   realizacją 

poniższego schematu (cyklu):

1. Zaprojektowanie i zdefiniowanie przypadków testowych dla wymagań – nie należy 

od razu opisywać wszystkich cech: należy zacząć od najprostszych i najmniejszych 
kroków, stopniowo zwiększając ich zakres. Na tym etapie kompilacja utworzonego 

kodu zakończy się niepowodzeniem z powodu braku testowanych obiektów.

2. Opierając się na klasach i metodach opisanych w testach należy stworzyć szkielet 

klas, które będą realizowały przypadki testowe. Przed dodaniem jakiejkolwiek 
funkcjonalności należy uruchomić testy, aby sprawdzić, czy testy są wykonywane 

(bez   zdefiniowanych   treści   testowanych   metod,   testy   zakończą   się 
niepowodzeniem).

3. Implementacja kolejnych wymagań. Po każdej zmianie należy uruchomić testy – 

liczba błędów będzie maleć z każdą poprawną implementacją. Po pozytywnym 

wykonaniu   wszystkich   testów   można   zdefiniować   kolejne   przypadki   testowe, 
bardziej kompleksowe.

Często podczas tworzenia czy rozwijania jakiegoś systemu klient nalega na zmianę 

istniejącej już funkcjonalności. Jeśli na tym etapie pominie się stworzenie odpowiednich 
testów,   podczas   modyfikacji   kodu   mogą   powstać   nowe,   trudne   do   znalezienia   i 

poprawienia w późniejszym etapie błędy. Ab tego uniknąć, należy:

1. Poprawić   testy,   aby   w   pełni   odzwierciedlały   zmianę   specyfikacji   testwoanego 

fragmentu   aplikacji.   Jeśli   po   uruchomieniu   testów,   nie   występują   błędy, 
prawdopodobnie zmienione przypadki testowe nie są wykonywane.

2. Poprawiać implementację tak, by testy nie wykazywały już żadnych błędów. W 

przypadku zmiany funkcjonalności wymaganej w innej części programu, przypadki 

background image

testowe uruchamiane w owym fragmencie systemu zakończą się niepowodzeniem, 

co pozwoli uniknąć w dużej mierze psucia istniejącego kodu.

W razie, gdy znaleziono błąd, którego do tej pory nie uwzględniały żadne testy, 

najpierw należy dodać przypadki testowe, które powinny go wykryć. Następnie trzeba 

uruchomić testy, aby sprawdzić, czy nowy zestaw faktycznie wykrywa błąd. Poprawka 
zostaje zakończona, gdy wszystkie testy kończą się sukcesem.

background image

Narzędzia do automatycznego testowania.

Automatyczne  testowanie,  jako jeden z  głównych postulatów XP,  doczekał  się 

wielu   narzędzi   ułatwiających   jego   realizację   podczas   pracy   z   każdym   językiem 

(obiektowym). Oto najpopularniejsze rozwiązania dla wybranych języków.

Java – JUnit

JUnit jest  najpopularniejszą   biblioteką służącą  do konstruowania  testwów.  Jej 

popularność i powszechne użycie (w tym wypadku idące w parze z jakością) jest przede 
wszystkim wynikiem popularności jej autorów, którymi są m. in. Kent Beck (jeden z 

największych autorytetów eXtreme Programming) oraz Erich Gamma (Gang of Four) – 
zresztą   kostrukcja   JUnit   jest   konsekwencją   jej   pochodzenia;   Kent   Beck   napisał   jej 

piwerszą implementację w Smalltalk'u.

Mając   wydzielony   model   aplikacji,   można   przygotować   przypadki   testowe. 

Załóżmy, że należy napisać klasę operującą na napisach.

public class NapisTest extends TestCase

{

public void runTest()

{

Napis nDu = new Napis(

“du”

);

Napis nPa = new Napis(

“pa”

);

Napis nDupa = new Napis(

“dupa”

);

nDu.zloz(nPa);
assert(nDu.equals(nDupa));

}

}

Stworzona specjalna klasa testowa, która dziedziczy z klasy TestCase nalezącej 

do  JUnit. Metoda assert(), pochodząca z tej klasy, sprawdza czy wyrażenie będące 
jej argumentem jest prawdą.

Napisana klasa zawiera tylko jeden test. Każdy następny wygląda podobnie:

1. stworzenie testowanych obiektów;

2. wykonanie wymaganych operacji;
3. zwolnienie zasobów (jeśli zostały jakieś pobrane).

Kod z podpunktów 1. i 2. może być współdzielony między wszystkimi testami. 

Umieszcza się go w metodach odpowiednio setUp() i tearDown(). Zbiór testowanych 
obiektów nazywany jest Fixture.

public class

 

NapisTest extends TestCase

{

Napis nDu;

Napis nPa;

   

protected void setUp()

   

{

nDu = new Napis(

“du”

);

      

nPa = new Napis(

“pa”

);

   

}

   

background image

protected void tearDown()

{

      

/* brak zasobów do zwolnienia */

   

}

public void testZloz()

   

{

      

Napis nDupa = new Napis(

“dupa”

);

      

nDu.zloz(nPa);

      

assert(nDu.equals(nPa));

   

}

   

public void testPodnapis()

   

{

      

Napis wynik = new Napis(

“p”

);

      

nDu = nPa.podnapis(0,1);

      

assert(nDu.equals(wynik));

   

}

}

W   celu   uruchomienia   kodu,   należy   skonstruować   instancje   testów   z   podanymi 

nazwami metod z testem. Metoda  run(),  korzystając z refleksji w Javie, wywołuje 
odpowiednią metodę testową.

TestResult result = (new NapisTest(

"testZloz"

)).run();

TestResult result =  (new NapisTest(

"testPodnapis"

)).run();

Domyślna   implementacja   metody  suite()  tworzy   zbiór   testów   na   podstawie   nazw 
metod klasy testowej - wybierane są wszystkie zaczynające się od "test". Przed każdą 

metodą testową wykonywany jest kod metody setUp(), a zaraz po zakończeniu metody 
testowej   wykonywana   jest   metoda  tearDown().   Dzięki   temu   przypadki   testowe   są 

odizolowane od siebie i wykonanie jednego nie ma wpływu na pozostałe.

Po przygotowaniu kilku przypadków testowych, można je pogrupować w zbiory 

przypadków testowych.

public static Test suite()

{

TestSuite suite= new TestSuite();   

   

suite.addTest(new NapisTest(

"testZloz"

));   

     suite.addTest(new NapisTest(

"testPodnapis"

));   

      

return suite;

}

Python - PyUnit

PyUnit to bazujący na JUnit moduł do testwoania kodu napisanego w Pythonie, 

będący integralną częścią standardowej biblioteki Pythona 2.1.

Podobnie   jak   JUnit,   PyUnit   używa   klasy  TestCase  z   modułu  unittest 

reprezentującej przypadki testowe i tak jak w JUnit, aby napisać własne testy, trzeba 

rozszerzyć tę klasę. Przykładowa klasa testowa pochodzi z dokumentacji PyUnit:

 

import unittest

background image

class DefaultWidgetSizeTestCase(unittest.TestCase):

def runTest(

self

):

widget = Widget(

"The widget"

)

assert widget.size() == (50,50), 

'incorrect default size'

Aby utworzyć instancję tak zdefiniowanego przypadku testowego, trzeba wywołać 

konstruktor stworzonej klasy testowej bez argumentów:

testCase = DefaultWidgetSizeTestCase()

Analogicznie do JUnit tworzy się kod odpowiedzialny za tworzenie testowanych 

obiektów, zwalnianie zasobów i implementuje część Fixtures:

import unittest

class WidgetTestCase(unittest.TestCase):

            def setUp(

self

):

                self.widget = Widget(

"The widget"

)

            def tearDown(

self

):

                self.widget.dispose()
                self.widget = 

None

            def testDefaultSize(

self

):

                assert self.widget.size() == (50,50), 

'incorrect default size'

            def testResize(

self

):

                self.widget.resize(100,150)

                assert self.widget.size() == (100,150), \
                       

'wrong size after resize'

            

Stworzenie instancji testów w PyUnit wygląda następująco: 

defaultSizeTestCase = WidgetTestCase(

"testDefaultSize"

)

resizeTestCase = WidgetTestCase(

"testResize"

)

Tak zdefiniowane testy można dodać do zbioru przypadków testowych:

        

widgetTestSuite = unittest.TestSuite()

widgetTestSuite.addTest(WidgetTestCase(

"testDefaultSize"

))

widgetTestSuite.addTest(WidgetTestCase(

"testResize"

))

Wygodnie jest stworzyć obiekt zwracający zestaw testów z wcześniej zdefiniowanych 

przypadków testowych:

       def suite():
           suite = unittest.TestSuite()

           suite.addTest(WidgetTestCase(

"testDefaultSize"

))

           suite.addTest(WidgetTestCase(

"testResize"

))

           return suite
    

background image

Podsumowanie

Jak widać na przykładach, automatyczne testowanie nie jest zadaniem 

wymagającym ponadprzeciętnych zdolności programistycznych,  zwłaszcza, gdy używa 

się narzędzi testujących dla używanego języka, bazujących na JUnit – wówczas 
tworzenie testów polega na stosowaniu tego samego, powszechnego schematu z 

uwzględnieniem jedynie składni danego języka.

Podstawowym “mankamentem” jest zwiększona ilość kodu potrzebna na dodanie 

nowej funkcjonalności (każdorazowe tworzenie przypadków testowych przed 
implementacją poszczególnych założeń specyfikacji). Jednak ów “mankament” zostanie 

niejednokrotnie wynagrodzony dzięki uniknięciu wielu godzin poszukiwania i naprawiania 
błędów, które dzięki automatyzacji testowania są natychmiastowo wyłapywane.

Dodatkowo programowanie ukierunkowane testami (Test-Driven Development – 

TDD) opierające się na technice automatycznego testowania, nie jest jedynie “kolejnym 

sposobem samego programowania”, ale jest także swego rodzaju sposobem 
projektowania systemu i gwarantem spełniania przez niego założonej specyfikacji, która 

jest zdefiniowana w przypadkach testowych.


Document Outline