PHPUnit
PHPUnit służy do przeprowadzania tzw. testów jednostkowych. Pojedynczy test weryfikuje poprawność działania jednego elementu (jednostki) projektu. Jednostką może być np. metoda klasy, albo dowolny inny, w miarę samodzielny i dający się rozsądnie przetestować element. Zbiór testów pozwala przetestować cały komponent na zasadzie: jeśli wszystkie jego jednostki (metody) dają oczekiwane wyniki, to można się spodziewać, że jego implementacja pozbawiona jest widocznych błędów. PHPUnit wykonuje każdy test, a następnie porównuje uzyskany w nim wynik z oczekiwanym. Na podstawie tego ustala, czy test przeszedł, czy nie. Na końcu otrzymujemy raport z informacjami o ilości zaliczonych testów i o ewentualnych napotkanych błędach.
Dużą zaletą pakietu jest szeroka paleta opcji pozwalająca na budowanie złożonych środowisk testowych dla bardzo dużych aplikacji, a także obsługa obiektów mock czy generowanie raportów pokrycia kodu testami (w połączeniu z XDebug).
Instalacja PHPUnit
Instalację omówię w skrócie, mając nadzieję, że czytają to głównie osoby, które o PHPUnit już jednak słyszały i coś w nim robiły, a teraz pragną zobaczyć, jak z tego złożyć coś sensownego. Niemniej warto wspomnieć lub przypomnieć, iż twórcy zalecają instalowanie swego projektu przy pomocy instalatora PEAR, gdzie musimy wydać następujące komendy:
pear channel-discover pear.phpunit.de pear install phpunit/PHPUnit
Aplikację obsługuje się z poziomu wiersza poleceń. Wpiszmy w nim phpunit. Jeśli otrzymaliśmy wykaz dostępnych opcji, oznacza to, że system testujący został zainstalowany.
Krok 1 - struktura katalogów
Rozpoczynamy od przygotowania odpowiedniej struktury katalogowej. PHPUnit umożliwia budowanie złożonych środowisk testowych, w których możemy precyzyjnie określać, czy chcemy przetestować wszystko, czy też interesuje nas pojedynczy komponent (bo np. aktualnie tylko nad nim pracujemy). Jednak aby ta sztuczka się udała, konieczne jest wprowadzenie pewnego ładu i porządku. Najlepiej, aby struktura katalogowa pakietu testowego odzwierciedlała układ katalogów naszego projektu. Weźmy pod uwagę następującą aplikację:
/MojaAplikacja
/Pertrozer
/Standard.php
/Agrippy.php
/Sprutifier
/Jawback.php
/Clonetrick.php
/Gepertor.php
/Ummobogorotitor.phpProponowana przeze mnie struktura to:
/tests
/Extra
/Package
/Pertrozer
/StandardTest.php
/AgrippyTest.php
/Sprutifier
/JawbackTest.php
/ClonetrickTest.php
/Stuff
/Stuff1Test.php
/GepertorTest.php
/UmmobogorotitorTest.php
/AllTests.php
/AllTests.php
/Bootstrap.php
/config.xmlZałożenia związane z taką strukturą oraz jej omówienie:
- Testy znajdują się w katalogu /tests/Package, którego struktura katalogowa jest zbliżona do struktury naszego projektu.
- Testy dla przykładowego pliku Foo.php powinny być umieszczone w pliku FooTest.php w odpowiednim folderze struktury /tests/Package.
- Struktura /tests/Package może zawierać także pliki testów i katalogi, które nie mają odpowiedników w oryginale. Mogą one zawierać testy dla jednostek innych, niż klasy.
- W katalogu /tests/Extra możemy umieścić dodatkowe implementacje wymagane przez sam system testujący. Przykładowe zastosowanie podam dalej.
- Pozostałe widoczne pliki to elementy naszego systemu testującego, którymi zaraz się zajmiemy.
Organizowanie zestawu testów
Naszym pierwszym zadaniem jest implementacja obu plików AllTests.php, które reprezentują zestaw testów. Za ich pomocą będziemy mogli uruchomić wszystkie testy, jakie wchodzą w skład naszego pakietu. Zajmijmy się na początku plikiem /tests/AllTests.php:
<?php /** * Zestaw testow uruchamiajacy calosc. * * @author Tomasz "Zyx" Jędrzejewski */ require_once 'PHPUnit/Framework.php'; require_once './Package/AllTests.php'; class AllTests { /** * Konfiguruje obiekt zestawu testow * * @return PHPUnit_Framework_TestSuite */ public static function suite() { $suite = new PHPUnit_Framework_TestSuite('Package'); $suite->addTest(Package_AllTests::suite()); return $suite; } // end suite(); } // end AllTests;
Plik ten istnieje przede wszystkim dla naszej wygody, abyśmy nie musieli zbyt głęboko schodzić w strukturę katalogową. Mamy tu jedną klasę z jedną statyczną metodą o nazwie suite(), której zadaniem jest stworzenie i zwrócenie obiektu klasy PHPUnit_Framework_TestSuite. Do zestawu dodajemy też właściwy zestaw zwracany przez Package_AllTests::suite().
Teraz możemy utworzyć plik /tests/Package/AllTests.php o dość podobnej, ale już nieco bardziej rozbudowanej strukturze:
<?php /** * Odpala wszystkie testy w katalogu /Package * * @author Tomasz "Zyx" Jędrzejewski */ require_once('GepertorTest.php'); require_once('UmmobogorotitorTest.php'); class Package_AllTests extends PHPUnit_Framework_TestSuite { /** * Skofiguruj zestaw testow. * @return Package_AllTests */ public static function suite() { $suite = new Package_AllTests('Package'); $suite->addTestSuite('Package_GepertorTest'); $suite->addTestSuite('Package_UmmobogorotitorTest'); return $suite; } // end suite(); /** * Wykonywana przed testami. */ protected function setUp() { // jakis kod startowy } // end setUp(); /** * Konczy procedure testowa */ protected function tearDown() { // jakis kod koncowy } // end tearDown(); } // end Package_AllTests;
Tym razem zamiast tworzyć pustą klasę, skorzystaliśmy z dziedziczenia. Dzięki temu mamy do dyspozycji metody setUp() i tearDown(), wykonywane odpowiednio przed rozpoczęciem i po zakończeniu wszystkich testów. Możemy tam zamieścić np. kod przygotowujący środowisko albo sprzątający. W metodzie suite() podłączamy już poszczególne pliki testów, najpierw dołączając je instrukcją require_once, a później rejestrując nazwy zawartych tam klas metodą addTestSuite(). Możemy też załączyć tu kolejny, jeszcze bardziej szczegółowy zestaw testów dotyczący np. podkatalogu.
Krok 2 - piszemy test
Właściwe testy umieszczamy w klasie dziedziczącej po PHPUnit_Framework_TestCase. Pojedynczy test reprezentowany jest przez metodę o nazwie testCostam, która domyślnie nie pobiera żadnych argumentów. Także i tutaj mamy dostęp do metod setUp() i tearDown(), które są wykonywane przed i po każdym teście. Oto przykładowy plik GepertorTest.php:
<?php /** * Testy dla komponentu Gepertor * * @author Tomasz "Zyx" Jędrzejewski */ class Package_GepertorTest extends PHPUnit_Framework_TestCase { private $_gepertor; protected function setUp() { $this->_gepertor = new MojaAplikacja_Gepertor; } // end setUp(); protected function tearDown() { $this->_gepertor->dispose(); unset($this->_gepertor); } // end tearDown(); public function testMethod1() { $this->assertEquals('Foo', $this->_gepertor->method1()); } // end testMethod1(); public function testMethod2() { $this->assertEquals('Bar', $this->_gepertor->method2()); } // end testMethod2(); } // end Package_GepertorTest;
Klasa PHPUnit_Framework_TestCase dostarcza szeregu metod do porównywania uzyskanego wyniku z oczekiwanym. Ich nazwy rozpoczynają się od assert, a jedną z nich jest assertEquals(), która zalicza test, jeśli uzyskaliśmy określony w pierwszym argumencie oczekiwany rezultat tekstowy. Innymi przydatnymi metodami są np. assertTrue(), gdzie oczekiwana wartość to prawda logiczna, czy fail(), która bezwarunkowo zawala test z podanym komunikatem błędu. Może być to przydatne, gdy testujemy wyjątki, np:
try { testowanyKod(); } catch(Exception $exception) { $this->fail('Niestety, dostalem wyjatkiem '.get_class($exception).' z wiadomoscia '.$exception->getMessage()); }
Przykład, gdy nieotrzymanie wyjątku jest błędną sytuacją:
try { testowanyKod(); } catch(SpodziewanyWyjatek $exception) { return true; } $this->fail('Powinienem otrzymac wyjatek SpodziewanyWyjatek');
Krok 3 - konfiguracja i ładowanie projektu
Mogłoby się wydawać, że to już wszystko, ale przed nami jeszcze trochę pisania. Przede wszystkim, nie będziemy raczej ładować właściwych plików naszej aplikacji ręcznie przez require_once. Jak widać w kodzie, nawet tak nie próbuję robić. Obecnie we frameworkach powszechnie stosuje się autoloadery, dlatego byłoby fajnie, gdybyśmy i my mogli je w którymś miejscu zainicjować. Pojawia się tylko pytanie - gdzie. Nie zrobimy tego w plikach testów, np. GepertorTest.php, ponieważ wtedy zamkniemy sobie możliwość uruchamiania wszystkich testów naraz. Nie umieścimy też tego w zestawach testów, ponieważ wtedy z kolei nie odpalimy pojedynczych testów.
Rozwiązanie to stworzenie dodatkowego pliku PHP, który w naszej strukturze katalogowej oznaczony jest jako /tests/Bootstrap.php. Umieszamy tam konfigurację naszego autoloadera w formie zwykłego kodu PHP, bez żadnego opakowywania w klasy, funkcje itd. Będziemy mogli poinformować PHPUnit o jego istnieniu za pośrednictwem dodatkowego przełącznika w wierszu poleceń lub w pliku konfiguracyjnym. Ja polecam tę drugą opcję, ponieważ można tam ustawić dużo więcej rzeczy. Taki podstawowy plik (/tests/config.xml) przedstawiam poniżej:
<phpunit bootstrap="Bootstrap.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnFailure="false"> <!-- tutaj konfiguracja pomniejszych elementow --> </phpunit>
Jako atrybuty głównego znacznika <phpunit> ustawiamy poszczególne parametry, m.in. sposób traktowania błędów PHP oraz lokalizację pliku Bootstrap.php. Tyle wystarczy, ale warto zainteresować się też dokładniej budową i możliwościami tego pliku opisanymi w dokumentacji - pozwalają one m.in. na skonfigurowanie sposobu tworzenia raportów czy obsługi pokrycia kodu.
Krok 4 - testowanie
To wszystko - nasz pakiet testowy jest już gotowy i możemy przystąpić do działania. Ponownie uruchamiamy wiersz poleceń i poleceniem cd przechodzimy do katalogu /tests. Możemy tutaj wydać następujące polecenia:
Odpal wszystkie testy: phpunit --configuration ./config.xml AllTests Odpal pojedynczy test: phpunit --configuration ./config.xml Package_GepertorTest
Powinniśmy wtedy otrzymać raport o wykonanych testach, który wygląda mniej więcej tak:
PHPUnit 3.3.9 by Sebastian Bergmann. ....... Time: 0 seconds OK (7 tests, 7 assertions)
Kropki oznaczają zdane testy. Jeśli dany test nawalił, w miejscu jego kropki pojawi się litera F (błędny wynik) lub E (błąd PHP), zaś my dodatkowo zostaniemy poinformowani o szczegółach błędu.
Niestandardowe rozwiązania i porady
Nie da się podać ogólnych wskazówek, jak powinien wyglądać test sprawdzający określoną funkcjonalność. Różne elementy wymagają różnych podejść i to głównie od naszego sprytu i inteligencji zależy, czy zastosowany przez nas chwyt w ogóle coś daje. Niektóre testy mogą być bardzo skomplikowane. Czytałem niedawno wpis jednego z autorów instalatora PEAR 2 o nazwie Pyrus poświęcony ich zestawowi testów, w którym konieczne jest łączenie się z zewnętrznymi witrynami, ściąganie fikcyjnych pakietów itd. przez co wykonanie całej, kompletnej procedury trwa prawie pół godziny. Pokazuje to, że testy wcale nie muszą być proste, łatwe i przyjemne.
Kiedy brałem się w listopadzie za pisanie testów jednostkowych dla Open Power Template'a, stanąłem przed koniecznością testowania składni języka i wszystkich jego instrukcji. Zorientowałem się, że tworzenie dla każdego testu osobnej metody i osobnego pliku szablonu będzie okropną męczarnią i na dłuższą metę doprowadzi do dużej nieczytelności kodu oraz częstego powtarzania tych samych fragmentów. Wtedy wpadłem na pomysł, by rozbudować PHPUnit o coś w rodzaju wirtualnego systemu plików, dzięki czemu wszystkie pliki wchodzące w skład jednego testu będą mogły być zapisane jako jeden plik testowy. Tutaj przydaje się katalog /tests/Extra przeznaczony właśnie na lokowanie podobnych wynalazków napisanych wyłącznie dla celów procedury testowej. Oto wygląd przykładowego testu, attribute_3.txt:
The test checks the attribute loops. >>>>templates/test.tpl <?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <opt:root> <foo> <opt:attribute name="$attributes.name" value="$attributes.value" opt:section="attributes" /> bar </foo> </opt:root> >>>>data.php $view->attributes = array(0 => array('name' => 'abc', 'value' => 'def'), array('name' => 'ghi', 'value' => 'jkm') ); >>>>expected.txt OUTPUT >>>>result.txt <foo abc="def" ghi="jkm"> bar </foo>
Na początku każdego pliku znajduje się ignorowany nagłówek z opisem, co dany test właściwie ma robić. Linijki >>>>nazwa rozpoczynają kolejne pliki o podanych nazwach. Mamy tutaj test/template.tpl z testowanym szablonem, data.php z kodem PHP i dodatkową konfiguracją, expected.txt z informacją o spodziewanej odpowiedzi (OUTPUT lub nazwa wyjątku) oraz result.txt z oczekiwanym kodem wynikowym. Do celów parsowania napisałem specjalną klasę implementującą potrzebne mi funkcje systemu plików (otwieranie pliku, czytanie, zamykanie) i zarejestrowałem ją jako strumień test://. W ten sposób zawartość testu mogę odczytywać zwykłymi funkcjami plikowymi PHP tak, jakbym faktycznie otwierał rzeczywiste pliki:
file_get_contents('test://expected.txt'); include('test://data.php');
Według mnie jest to świetny sposób testowania niektórych rzeczy związanych z systemem plików w sytuacjach, kiedy użycie rzeczywistych plików powodowałoby utratę czytelności testów. Jednak to nie wszystko. Skoro testy są właściwie plikami testowymi, wszystkie odpalane są i analizowane pod kątem poprawności wyniku w dokładnie taki sam sposób. Nie muszę tworzyć 150 metod w stylu testAttribute3() różniących się tylko nazwą wczytywanego pliku, lecz korzystam z tzw. data providers. Dostawca danych to zwyczajna metoda, która zwraca tablicę z zestawami danych do przetestowania. W moim przypadku są to po prostu nazwy kolejnych plików z testami. Umieszczam ją w mojej klasie testów:
public function instructionProvider() { return array(0 => array('attribute_1.txt'), array('attribute_2.txt'), array('attribute_3.txt'), array('block_1.txt'), array('block_2.txt'), // itd... ); } // end instructionProvider();
Teraz mogę podpiąć taki zestaw pod pojedynczą metodę testu:
/** * @dataProvider instructionProvider */ public function testInstruction($filename) { // tutaj kod ladujacy i testujacy podany w argumencie plik. } // end testInstruction();
PHPUnit potraktuje każdy taki zestaw jak osobny test, lecz wszystkie one będą wykonane przy pomocy jednej i tej samej metody.
Zakończenie
PHPUnit to bardzo rozbudowana biblioteka o dużych możliwościach, dlatego w tym wpisie nie byłem w stanie opisać ich wszystkich. Jednak myślę, że podane informacje okażą się przydatne i pomogą Ci zbudować porządny system automatycznego testowania dla Twoich skryptów.






Napisał noose w wtorek, 28 lipca 2009 o 13:58
Do testowania wyjątków można też stosować @expectedException (http://www.phpunit.de/manual/3.4/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.exceptions)