Inżynieria Oprogramowania 2013/14 LABARATORIUM 9 TESTY JEDNOSTKOWE JUNIT 3.8 Hierarchia klas: TestCase klasa testująca, będąca klasą bazową dla wszystkich przypadków testowych. Zawiera przypadki testowe zapisane w postaci metod (przypadek testowy to pojedyncza metoda, nie klasa. Udostępnia domyślne asercje. Razem z TestSuite stanowi implementację interfejsu Test. Możliwe jest tworzenie zestawów testów (suite) i uruchamianie ich w identyczny sposób jak pojedynczej klasy TestCase, struktura drzewiasta, którą tworzą pozwala na jednorodne zarządzanie testami (wzorzec Composite) TestResult jest klasą przechowującą wyniki wykonanych przypadków Testowych, tworzoną i wypełnianą danymi przez wykonywane klasy TestCase.
Struktura klasy testującej: Konstruktor przyjmuje parametr będący nazwą przypadku testowego (podawany opcjonalnie); Metoda setup() (dziedziczona po klasie TestCase) odpowiada za inicjację każdego przypadku testowego. Wykonywana jest przed każdym przypadkiem testowym. Tworzy obiekt testowany oraz obiekty niezbędne do jego uruchomienia. Przypadki testowe są implementowane w pojedynczych metodach klasy testującej (zgodność z konwencją JUnit 3.x: brak przyjmowanych parametrów i zwracanych wartości, nazwa z przedrostkiem 'test, w przeciwnym razie nie będą rozpoznawane jako przypadki testowe). Metoda teardown(), kończąca wykonanie każdego przypadku testowego (przywrócenie stanu sprzed wykonania testu) jest komplementarna do metody setup(). Służy zwalnianiu zasobów zaalokowanych przez przypadek testowy (rozłączenie połączenia sieciowego lub do bazy) Przykład: Klasa testująca dla klasy RomanNumber, reprezentującej liczby rzymskie. W klasie RomanNumberTest znajduje się pole przechowujące referencję do obiektu testowanego, które jest inicjowane w metodzie setup(). Przypadek testowy o nazwie testfive()sprawdza, czy konwersja liczby arabskiej 5 na rzymską V jest dokonywana właściwie, niezgodność jest raportowana przez wyjątek. Metoda teardown() została zaimplementowana jedynie dla przejrzystości, gdyż usunięcie obiektu testowanego jest realizowane mechanizm garbage collector.
Asercje w klasie TestCase: Asercje są to metody pomocnicze służące ocenie poprawności działania oprogramowania poprzez sprawdzenie czy spełniony został zadany warunek. Dla wielu typów danych w Javie są to metody przeciążone, co pozwala lepiej odzwierciedlać różnice między porównywanymi danymi. Istnieje wersja każdej metody przyjmująca jako pierwszy parametr komunikat wyświetlany dla nieprawdziwej wartości asercji. asercje porównań (AssertEquals) za pomocą tej asercji można zweryfikować dowolny obiekt poprzez sprawdzenie czy wartości przekazane jako parametry są równe. W przypadku typów prymitywnych porównanie jest wykonywane za pomocą operatora ==, natomiast dla typów obiektowych wywoływana jest metoda equals() odpowiednich klas. W szczególności asercja ta może porównywać zawartość ciągów znakowych. Ma również wersję o sygnaturach odpowiednich dla wszystkich typów podstawowych boolean, int, short oraz object. W przypadku tablic typów podstawowych porównywane są referencje tablic, a nie ich zawartość: assertequals([string message], expected, actual) Przykład: assertequals( Wynikiem powinno być 3 1/3, 3.33, 10.0/3.0, 0.01); asercje referencji null o assertnull umożliwia sprawdzenie, czy obiekt jest równy null, czy podana referencja wskazuje na obiekt. assertnull([string message], object) o assertnotnull umożliwia sprawdzenie, czy obiekt jest różny od null. assertnotnull([string message], object)
asercje tożsamości o assertsame sprawdza czy expected i actual reprezentują ten sam obiekt, we wszystkich przypadkach stosuje operator ==. assertsame([string message], expected, actual) o assertnotsame sprawdza czy expected i actual reprezentują różne obiekty, we wszystkich przypadkach stosuje operator ==. assertnotsame([string message], expected, actual) asercje logiczne o asserttrue sprawdza, czy podany warunek logiczny jest prawdziwy. asserttrue([string message], boolean condition) o assertfalse sprawdza, czy podany warunek logiczny jest nieprawdziwy. assertfalse([string message], boolean condition) bezwarunkowe niepowodzenie o fail skutkuje natychmiastowym negatywnym zakończeniem testu i (opcjonalnym) komunikatem message. Stosowany w celu zaznaczenia sekcji kodu, których osiągnięcie jest niepożądane (obsługa wyjątku). fail([string message]) Tworzenie obiektu testowanego: Nie powinno się odbywać w konstruktorze TestCase (w przypadku wystąpienia wyjątku zostanie on mylnie zinterpretowany przez środowisko JUnit (brak informacji wskazującej na przyczynę błędu)! o Konstruktor klasy testującej powinien zostać niemal całkowicie pominięty (każdy wyjątek zgłoszony w konstruktorze przerywa proces tworzenia obiektu.
Właściwym miejscem tworzenia obiektu testującego jest metoda testująca setup: Po przechwyceniu wyjątek zostanie poprawnie wyświetlony z kompletną informacją o przyczynie błędu: Przypadki testowe Kolejność wykonania przypadków testowych jest dowolna, gdyż są z założenia wzajemnie niezależne (każdy przypadek testowy poprzedzony jest metodą setup(), a po nim wywoływana jest metoda teardown()).kolejność przypadków w pliku nie ma żadnego znaczenia dla środowiska JUnit. Ustalenie kolejności przypadków testowych przez zastosowanie metody statycznej suite() (uruchomienie przypadków testowych przez środowisko JUnit). Kolekcja przypadków testowych przechowywana jest w obiekcie klasy TestSuite w sposób uporządkowany (wykonywanych w określonej kolejności). Metoda suite() tworzona w klasie testującej definiuje kolejność przypadków testowych w suicie ( w tym kolejność ich wykonania).
Testy nie powinny być zależne od zasobów zewnętrznych (dane do testowania w pliku zewnętrznym), gdyż mogą one nie istnieć (konsekwencja pracy nad jednym projektem przez kilku programistów). Niepowodzenie odczytu może mylnie wskazywać na błąd przypadku testowego. Częstą praktyką jest umieszczenie danych bezpośrednio w klasie testującej (pole lub oddzielna klasa wewnętrzna) wymagana jest ponowna kompilacja programu przy zmianie danych, dane testowe są zawsze dostępne. Plik z danymi można także dołączyć do pliku JAR z klasami, a odczytać go korzystając z class loader, pomijając system plików: adresowanie plików wewnątrz pliku JAR bez znajomości jego położenia. Testy nie powinny zależeć od kontekstu i konfiguracji środowiska, jak format prezentacji czasu i daty, bieżąca godzina lub strefa czasowa (fałszywe komunikaty błędów). Testy należy wykonywać względem bieżącej konfiguracji komputera, nie zaś stałych ustawień w treści przypadku testowego.
Testowanie wyjątków Przypadek testowy sprawdza, czy w odpowiedzi na konkretne wywołanie metody w obiekcie testowanym zostanie zgłoszony wyjątek (brak wyjątku jest błędem). umieszczenie tej metody wewnątrz właściwie skonfigurowanej klauzuli try..catch. Wewnątrz klauzuli try wywoływana jest metoda, która powinna zgłosić wyjątek, a zaraz po niej metoda fail(). W klauzuli catch przechwytywany jest wyjątek zgłaszany przez testowaną metodę, a następnie jest on ignorowany. W ten sposób, jeżeli wyjątek się pojawi, wówczas zamiast metody fail() wykonana zostanie klauzula catch, która zignoruje wyjątek. Natomiast jego brak spowoduje zgłoszenie błędu za pomocą metody fail(). użycie specjalnej klasy ExceptionTestCase. Konstruktor tej klasy przyjmuje dwa argumenty: nazwę przypadku testowego oraz klasę wyjątku, który należy przechwycić. Testowanie liczb zmiennoprzecinkowych Wiele liczb zmiennoprzecinkowych jest reprezentowanych niedokładnie, stąd tez asercje tego typu są przeciążone (dodatkowy parametr oznaczający największą dopuszczalną różnicę pomiędzy wartością oczekiwaną a uzyskaną). Asercja zamiast porównywać wartości sprawdza, czy są one wystarczająco bliskie siebie. Parametr ten powinien przyjmować małe wartości.
Struktura katalogów z testami (rozdzielenie testów od kodu) Każdej klasie produkcyjnej odpowiada klasa testująca, obie powinny znajdować się w tych samych pakietach (dostęp do składowych o pakietowym zakresie widoczności package private), nie jest to jednak optymalne rozwiązanie. Stosuje się więc dwie równoległe struktury pakietów: dla klas produkcyjnych i testujących. Testowanie metod niepublicznych 1. Należy testować jedynie metody publiczne; tylko one wchodzą w oddziaływanie z innymi obiektami. 2. Należy testować wszystkie klasy nietrywialne bez względu na ich zakres widoczności. Testowanie klas prywatnych i chronionych umożliwia w Javie mechanizm JavaReflection (zmiana poziomu dostępu do pola lub metody przez JunitX - dodatek do JUnit, klasa PrivateAccessor)
Wady JUnit 3.8 sztywne konwencje nazewnicze przypadków testowych, konieczność dziedziczenia po klasie TestCase (dodatkowa zależność), często niewystarczająca kontrola kolejności wykonywania przypadków testowych jedna metoda inicjująca i jedna finalizująca dla wszystkich przypadków testowych (duplikacja kodu w złożonych przypadkach) brak dodatkowych funkcji obsługi limitów czasowych (wbudowanej obsługi testowania wyjątków, braku mechanizmu konfiguracji przypadków testowych, poza niektórymi rozszerzeniami proponowanymi przez niezależnych autorów)