2 czerwca 2009

Testowanie i automatyzacja testów part.I

Wielu z programistów wie, jak istotne i ważne są testy oprogramowania. Jednak między „nami” są osoby, które nie testują oprogramowania i nie posiadają na tyle szerokiej wiedzy, aby móc stosować wzorce projektowe, które mimo wszystko są integralną częścią procesów związanych z automatyzacją testów. Z doświadczenia… takie osoby, również do dzisiaj i ja… podczas projektowania oprogramowania tworzyliśmy tzw. „diabelski krąg”, z którego bardzo trudno się uwolnić. Poniżej znajdują się wymówki i stwierdzenia, które tworzą taki krąg:
  • „Nie mam czasu na testowanie”.
  • „Testowanie oprogramowania jest nudne i niepotrzebne”.
  • „Mój kod jest praktycznie bezbłędny, a z całą pewnością wystarczająco dobry”.
  • „Od testowania jest dział testów. Oni znają się na tym znacznie lepiej”.

Chęć pogłębienia wiedzy na ten temat zainicjowała powstanie właśnie tego artykułu. Wiele zdań i tekstu zostało przepisane ze źródeł. Jednak myślę, że udało mi się zebrać chociaż te najważniejsze wiadomości na temat testowania oprogramowania w jedną całość. Mimo tego, że w sieci jest dużo takich informacji, mam nadzieję, że artykuł ten pomoże zrozumieć tajniki testowania, które przedstawiłem nie tylko w przykładach z Java wielu początkującym programistom.

Wykorzystana literatura:

„Im więcej tworzysz testów, tym stajesz się mniej produktywny, a twój kod staje
się mniej stabilny”.
Erich Gamma

Alternatywne działania:

public static void main(...) throwsException{
//wołanie metod, sprawdzenie wyników
//w razie błędu rzuca wyjątek
System.out.println("Test OK");
}

Wady tego rozwiązania:

  • Klasa najmniejszą jednostką testowania
  • Przerwanie przy pierwszym błędzie
  • Kod testowy obecny w kodzie wynikowym
  • Brak mechanizmu do uruchamiania
  • Brak mechanizmu dla raportów

Wprowadzenie:

Pojecie testowania oprogramowania jest tak obszerne, że ciężko byłoby przedstawić wszystkie aspekty związane właśnie z tym zagadnieniem. Jednak postaram się ująć w tym artykule kluczowe wiadomości, które wyjaśnią istotę testowania i praktycznego zastosowania.

Ogólnie opiszę tematykę standardów w testowaniu i ich podział. Bardziej natomiast skupię się nad praktycznym zastosowaniem procesów testowania modułowego. Omówię biblioteki „Test-Unit” na przykładzie PHPUnit, która to wywodzi się z biblioteki JUnit.

Wyjaśnię kilka terminów związanych z testowaniem, np.: Refactoring.

Omówię wykonanie i ocenianie przypadków testowych, automatyczne generowanie klas przypadków testowych.

Przedstawię sposób generowania dokumentacji za pomocą funkcji TestDox, która jest częścią biblioteki PHPUnit.

To wszystko postaram się wyjaśnić na przykładzie klasy KontoBankowe w PHP i klasy DateStr w Java.

Podział testowania:

  • Testy modułowe
  • Testy integracyjne zewnętrzne
  • Testy funkcjonalne
  • Testy systemowe
  • Testy integracyjne zewnętrzne
  • Testy akceptacyjne (testy alfa / testy beta)

Testy modułowe:

Tę grupę testów przedstawię od strony praktycznej w tym artykule.

Jest to metoda testowania tworzonego oprogramowania poprzez wykonywanie testów weryfikujących poprawność działania pojedynczych elementów (jednostek) programu - np. metod lub obiektów w programowaniu obiektowym lub procedur w programowaniu proceduralnym. Testowany fragment programu poddawany jest testowi, który wykonuje go i porównuje wynik (np. zwrócone wartości, stan obiektu, wyrzucone wyjątki) z oczekiwanymi wynikami - tak pozytywnymi jak i negatywnymi (niepowodzenie działania kodu w określonych sytuacjach również może podlegać testowaniu).

Zaletą testów jednostkowych jest możliwość wykonywania na bieżąco w pełni zautomatyzowanych testów na modyfikowanych elementach programu, co umożliwia często wychwycenie błędu natychmiast po jego pojawieniu się i szybką jego lokalizację, zanim dojdzie do wprowadzenia błędnego fragmentu do programu. Testy jednostkowe są również formą specyfikacji. Z powyższych powodów są szczególnie popularne w programowaniu ekstremalnym.

Testy integracyjne:

Po odrębnym przetestowaniu komponentów programu, należy je zintegrować w gotowy lub częściowo ukończony system. Ten proces integracji polega na zbudowaniu systemu i przetestowaniu otrzymanego systemu w poszukiwaniu problemów związanych z interakcjami komponentów.

Testy integracyjne należy opracować na podstawie specyfikacji systemu. Testowanie integracyjne należy rozpocząć natychmiast po powstaniu zdatnych do użycia wersji komponentów systemu. Największą trudnością testowania integracyjnego jest lokalizowanie błędów wykrytych w trakcie tego procesu.

Między komponentami systemu zachodzą złożone interakcje. Znalezienie błędu, będącego przyczyną anomalii wykrytych w danych wyjściowych, może być trudne. Aby ułatwić ową lokalizację błędu, zawsze należy stosować przyrostowe podejście do interakcji i testowania systemu. Na początku należy zintegrować pewną minimalną konfigurację systemu i przetestować otrzymany system. Następnie trzeba dodawać kolejne komponenty do tej minimalnej konfiguracji i testować po każdym dodanym przyroście.

Testowanie integracyjne wykonywane jest w celu wykrycia błędów w interfejsach i interakcjach pomiędzy modułami (assembly testing).

Przykład: Testujemy komunikację pomiędzy modułem przechowującym i udostępniającym zbiór parametrów, a modułem używającym tych parametrów przy inicjacji, np. do wypełnienia pól formularza domyślnymi wartościami.

Współczesne aplikacje składają się przeważnie z wielu współpracujących systemów, należy więc sprawdzić czy komunikacja pomiędzy nimi nie jest zakłócona.

Podejście do testów integracyjnych:

Top - Down (od góry do dołu)

  • Moduły znajdujące się na najwyższym poziomie są testowane jako pierwsze
  • Moduły znajdujące się w hierarchii poniżej, zastępowane/symulowane są przez zaślepki (stubs)
  • Testowane moduły używane są do testowania niżej położonych komponentów
  • Proces testowy jest kontynuowany do momentu przetestowania komponentów znajdujących się na najniższym poziomie

Bottom - Up (od dołu do góry)

  • Najniżej położone komponenty testowane są jako pierwsze
  • Drivers(ang.) symulują komponenty położone wyżej w hierarchii
  • Testowane moduły używane są do testowania wyżej położonych komponentów
  • Proces testowy jest kontynuowany do momentu przetestowania komponentów znajdujących się na najwyższym poziomie

Big Bang (tłumaczenie jest adekwatne do tego, co dzieje się z systemem)

  • Błędy występujące w interfejsach komponentów wykrywane są w bardzo późnej fazie procesu testowego
  • Trudno jest określić miejsce, w którym występuje defekt. Czy przyczyna błędu leży w komponencie czy w interfejsie?
  • Istnieje wysokie prawdopodobieństwo niewykrycia krytycznych błędów, które mogą ujawnić się dopiero w wersji produkcyjnej systemu
  • Trudno upewnić się, czy wszystkie przypadki z poziomu testów integracyjnych są pokryte testami.

Testowanie funkcjonalne a systemowe:

Testy funkcjonale są częścią testów systemowych.

Testy funkcjonalne są oparte na wymaganiach funkcjonalnych aplikacji, podczas gdy testy systemowe obejmują o wiele szerszy zakres działań, min. testowanie funkcjonalności, wydajności, użyteczności, obciążenia i bazy danych.

Celem przeprowadzania testów systemowych jest sprawdzenie, czy zintegrowany już system spełnia wymagania funkcjonalne oraz wymagania systemowe zawarte w specyfikacji.

Testy akceptacyjne:

  • Walidacja systemu pod kątem zgodności z wymaganiami klienta, który w swoim środowisku wykonuje przypadki testowe przy udziale przedstawicieli projektu. Kiedy system zostaje zaakceptowany, następuje uruchomienie na środowisku produkcyjnym
  • Szczegółowe testowanie funkcjonalności całego, kompletnego systemu.
  • Wykonywane w środowisku, w którym system został utworzony (ang. Development environment).

Alfa testy (ang. alpha testing) - wykonywane w docelowym środowisku, w którym system będzie pracował testujący - zespół testerów

Beta testy (ang. beta testing) – docelowe środowisko pracy systemu (u użytkownika);testujący - użytkownik

Standardy w testowaniu

Podstawowym standardem dla testowania oprogramowania jest IEEE 829 – 1998 (829 Standard for Software Test Documentation). Jest to standard określający formę zbioru 8 dokumentów potrzebnych w każdej z faz testowania oprogramowania. W efekcie każdej z tych faz tworzony jest 1 dokument wynikowy. Standard ten określa dokładnie format dokumentów, jednak nie wymaga, aby wszystkie były wykonane. Nie zawiera także informacji o tym, co dokładnie mają zawierać.

Test Plan – dokument planowania zarządzania projektem, który składa się z informacji o tym, w jaki sposób będą prowadzone testy, kto będzie je przeprowadzał, co będzie testowane, jak długo potrwa cały proces oraz jaki będzie zakres testów.

Test Design Specification – szczegóły na temat warunków testowania, oczekiwanych wyników a także kryteriach przejścia testu.

Test Case Specification – specyfikuje dane testowe do użycia podczas wdrażania warunków testowania określonych w Test Design Specification.

Test Procedure Specification – zawiera szczegóły na temat przeprowadzenia każdego testu włączając w to założenia oraz poszczególne kroki testów.

Test Item Transmittal Report – zawiera raporty na temat czasu przejścia testowanych fragmentów oprogramowania między etapami.

Test Log – zawiera informacje o tym, które przypadki testowania zostały użyte, kto je użył i w jakim porządku oraz informacje o ich powodzeniu.

Test Incident Report – zawiera informacje o testach zakończonych niepowodzeniem. Informacje o wynikach oraz dlaczego dany test nie powiódł się.

Test Summary Report – raport ten zawiera wszystkie istotne informacje ujawnione podczas zakończonych testów oraz wyceny, jakości procesów testowania, jakości oprogramowania poddanego testowi, a także statystyki uzyskane z Incident Report. Raport referuje również do typów i czasu trwania wykonanych testów w celu usprawnienia wszelkich planów związanych z testami w przyszłości. Ostateczna forma dokumentu jest wykorzystywana w celach weryfikacji poprawności testowanego systemu względem wymagań zdefiniowanych przez zleceniodawców.

Do innych standardów związanych z testowaniem oprogramowania należą: IEEE 1008, IEEE 1012, BS 7925-1, BS 7925-2.

Najpierw test (ang. test-first)

W programowaniu ekstremalnym (ang. Extreme programming), które należy do tak zwanych lekkich procesów tworzenia oprogramowania, diabelski krąg, który wcześniej został przytoczony nie ma racji bytu. Spowodowane jest to tym, że zanim przejdzie się do tworzenia właściwego kodu oprogramowania, narzucono wykonanie testów w pierwszej kolejności. Takie podejście nazywa się „najpierw test”. Wpływ wszystkich zmian wprowadzonych w jednym miejscu na pozostały kod można skontrolować szybko i dokładnie. W podejściu „test-first”, produkcja kodu składa się z dwóch kroków:

  1. Przed przystąpieniem do pisania kodu właściwego, przygotowuje się specjalny test, który ma motywować działanie kodu. Programista formułujący test musi zapoznać się z wszystkimi wymaganiami dotyczącymi kodu, który ma przygotowywać.
  2. Kod produkcyjny tworzony jest tylko w takiej ilości, jakiej wymaga przygotowany wcześniej test. Inaczej, jeżeli test działa poprawnie to znaczy, że kod został ukończony.

Refaktoring, (ang. refactoring)

Refaktoring kodu jest praktycznie niewykonalny bez zautomatyzowanego wykonywania testów przygotowanych przez projektanta na poziomie modułów (ang. unit tests). Martin Fowler definiuje refaktoring, jako: „proces takiego modyfikowania oprogramowania, żeby jego zewnętrzne zachowanie się nie zmieniło, ale została poprawiona jego wewnętrzna struktura” [Fowler1999]. Do zmian w strukturze kodu można zaliczyć zmiany, takie jak zmiany nazw klas i metod albo wydzielanie kodu jednej klasy i przydzielanie innej.

Wcześniej pisałem już o testowaniu modułów, ale wspomniałem, że będę chciał rozwinąć ten wątek.

Pojęcie testów modułów pochodzi jeszcze z programowania proceduralnego, gdzie testowana jednostka programowa była utożsamiana z funkcją lub procedurą. Jeżeli chodzi o programowanie obiektowe to tutaj mamy większe pole do popisu. Mamy do dyspozycji pojedyncze metody, całe klasy a nawet kompletne jednostki systemu, które wszystkie mogą być poddawane testom modułów.

Testy modułów umożliwiają ciągły refaktoring kodu wymuszając poniższe reguły:

  1. Wszystkie testy działają prawidłowo.
  2. Kod spełnia wszystkie założenia projektu.
  3. W kodzie nie ma żadnych nadmiarowości.
  4. Po uwzględnieniu powyższych reguł w kodzie znajduje się najmniejsza możliwa liczba klas i metod.

Dopiero zautomatyzowane testy pozwalają na skuteczne wprowadzanie do kodu zmian niezbędnych do osiągnięcia celów nawet najprostszych projektów. Bez automatyzacji każda metoda każdej klasy musiałaby być ręcznie testowana przy każdej zmianie kodu danej klasy.